use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::sync::LazyLock;
pub(crate) const REWIND_MODE_KEY: KeyCode = KeyCode::Tab;
pub(crate) const REWIND_MODE_HELP: &str = "[Tab] mode";
pub(crate) const THEME_VARIANT_HELP: &str = "[Tab/Shift-Tab] tabs";
pub(crate) fn is_main_tab_cycle_key(key: KeyEvent) -> bool {
(key.code == KeyCode::BackTab
&& (key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT))
|| (key.code == KeyCode::Tab && key.modifiers == KeyModifiers::SHIFT)
}
pub(super) fn is_theme_variant_switch_key(key: KeyEvent) -> bool {
matches!(key.code, KeyCode::Tab | KeyCode::BackTab)
&& (key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum KeyCommand {
OpenActivityDetail,
SubmitPrompt,
InsertPromptNewline,
Exit,
ClearInputOrExit,
FocusPrompt,
FocusActivityTree,
CyclePrimaryAgent,
ToggleHelp,
ToggleActivityTreeModal,
ToggleSessionFilesModal,
ToggleHeaderModalsPinned,
MoveSelectionUp,
MoveSelectionDown,
CollapseSelected,
ExpandSelected,
ExpandAll,
CollapseAll,
SelectFailed,
SelectRunning,
ScrollDetailUp,
ScrollDetailDown,
PageDetailUp,
PageDetailDown,
ScrollTranscriptUp,
ScrollTranscriptDown,
MovePromptCursorLeft,
MovePromptCursorRight,
MovePromptCursorUp,
MovePromptCursorDown,
UndoPrompt,
RedoPrompt,
MovePromptWordLeft,
MovePromptWordRight,
DeletePromptWord,
DeletePromptNextWord,
ExtendPromptSelectionLeft,
ExtendPromptSelectionRight,
ExtendPromptSelectionUp,
ExtendPromptSelectionDown,
SelectAllPrompt,
CycleThinkingLevel,
OpenModel,
ToggleSummary,
ToggleSummaryModal,
ScrollSummaryUp,
ScrollSummaryDown,
PageSummaryUp,
PageSummaryDown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum KeyContext {
Global,
Prompt,
Transcript,
ActivityTree,
ActivityDetail,
Summary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct KeyPattern {
code: KeyCode,
modifiers: KeyModifiers,
}
impl KeyPattern {
const fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
Self { code, modifiers }
}
fn matches(self, key: KeyEvent) -> bool {
key.code == self.code && key.modifiers == self.modifiers
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct KeyBinding {
pub(crate) action: KeyCommand,
pub(crate) context: KeyContext,
pub(crate) key: KeyPattern,
pub(crate) label: &'static str,
pub(crate) description: &'static str,
}
pub(super) const DEFAULT_KEYBINDINGS: &[KeyBinding] = &[
KeyBinding {
action: KeyCommand::ToggleSummary,
context: KeyContext::Summary,
key: KeyPattern::new(KeyCode::Enter, KeyModifiers::NONE),
label: "Enter",
description: "start/stop summary",
},
KeyBinding {
action: KeyCommand::ScrollSummaryUp,
context: KeyContext::Summary,
key: KeyPattern::new(KeyCode::Up, KeyModifiers::NONE),
label: "Up",
description: "scroll summary up",
},
KeyBinding {
action: KeyCommand::ScrollSummaryDown,
context: KeyContext::Summary,
key: KeyPattern::new(KeyCode::Down, KeyModifiers::NONE),
label: "Down",
description: "scroll summary down",
},
KeyBinding {
action: KeyCommand::PageSummaryUp,
context: KeyContext::Summary,
key: KeyPattern::new(KeyCode::PageUp, KeyModifiers::NONE),
label: "PgUp",
description: "page summary up",
},
KeyBinding {
action: KeyCommand::PageSummaryDown,
context: KeyContext::Summary,
key: KeyPattern::new(KeyCode::PageDown, KeyModifiers::NONE),
label: "PgDn",
description: "page summary down",
},
KeyBinding {
action: KeyCommand::ToggleHelp,
context: KeyContext::Global,
key: KeyPattern::new(KeyCode::F(1), KeyModifiers::NONE),
label: "F1",
description: "toggle help",
},
KeyBinding {
action: KeyCommand::Exit,
context: KeyContext::Global,
key: KeyPattern::new(KeyCode::Char('d'), KeyModifiers::CONTROL),
label: "Ctrl-D",
description: "exit",
},
KeyBinding {
action: KeyCommand::ClearInputOrExit,
context: KeyContext::Global,
key: KeyPattern::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
label: "Ctrl-C",
description: "copy selected text; otherwise clear prompt, or exit when empty",
},
KeyBinding {
action: KeyCommand::FocusActivityTree,
context: KeyContext::Global,
key: KeyPattern::new(KeyCode::Char('t'), KeyModifiers::CONTROL),
label: "Ctrl-T",
description: "open Activity Tree modal",
},
KeyBinding {
action: KeyCommand::CyclePrimaryAgent,
context: KeyContext::Global,
key: KeyPattern::new(KeyCode::Char('p'), KeyModifiers::ALT),
label: "Alt-P",
description: "cycle primary agent selection",
},
KeyBinding {
action: KeyCommand::ToggleActivityTreeModal,
context: KeyContext::Global,
key: KeyPattern::new(KeyCode::Char('1'), KeyModifiers::ALT),
label: "Alt-1",
description: "show/hide Activity Tree",
},
KeyBinding {
action: KeyCommand::ToggleSessionFilesModal,
context: KeyContext::Global,
key: KeyPattern::new(KeyCode::Char('2'), KeyModifiers::ALT),
label: "Alt-2",
description: "show/hide Session Files",
},
KeyBinding {
action: KeyCommand::ToggleSummaryModal,
context: KeyContext::Global,
key: KeyPattern::new(KeyCode::Char('3'), KeyModifiers::ALT),
label: "Alt-3",
description: "show/hide Summary",
},
KeyBinding {
action: KeyCommand::ToggleHeaderModalsPinned,
context: KeyContext::Global,
key: KeyPattern::new(KeyCode::Char('a'), KeyModifiers::ALT),
label: "Alt-A",
description: "pin/unpin all column modals beside Transcript; pinned modals hide on Changes",
},
KeyBinding {
action: KeyCommand::FocusPrompt,
context: KeyContext::Global,
key: KeyPattern::new(KeyCode::Esc, KeyModifiers::NONE),
label: "Esc",
description: "focus prompt and close help",
},
KeyBinding {
action: KeyCommand::CycleThinkingLevel,
context: KeyContext::Global,
key: KeyPattern::new(KeyCode::Char('t'), KeyModifiers::ALT),
label: "Alt-T",
description: "cycle reasoning level",
},
KeyBinding {
action: KeyCommand::OpenModel,
context: KeyContext::Global,
key: KeyPattern::new(KeyCode::Char('m'), KeyModifiers::ALT),
label: "Alt-M",
description: "open model picker",
},
KeyBinding {
action: KeyCommand::SubmitPrompt,
context: KeyContext::Prompt,
key: KeyPattern::new(KeyCode::Enter, KeyModifiers::NONE),
label: "Enter",
description: "submit prompt",
},
KeyBinding {
action: KeyCommand::InsertPromptNewline,
context: KeyContext::Prompt,
key: KeyPattern::new(KeyCode::Enter, KeyModifiers::SHIFT),
label: "Shift-Enter",
description: "insert newline",
},
KeyBinding {
action: KeyCommand::InsertPromptNewline,
context: KeyContext::Prompt,
key: KeyPattern::new(KeyCode::Char('j'), KeyModifiers::CONTROL),
label: "Ctrl-J",
description: "insert newline (portable fallback)",
},
KeyBinding {
action: KeyCommand::InsertPromptNewline,
context: KeyContext::Prompt,
key: KeyPattern::new(KeyCode::Enter, KeyModifiers::CONTROL),
label: "Ctrl-Enter",
description: "insert newline (requires terminal support)",
},
KeyBinding {
action: KeyCommand::MovePromptCursorLeft,
context: KeyContext::Prompt,
key: KeyPattern::new(KeyCode::Left, KeyModifiers::NONE),
label: "Left",
description: "move prompt cursor left",
},
KeyBinding {
action: KeyCommand::MovePromptCursorRight,
context: KeyContext::Prompt,
key: KeyPattern::new(KeyCode::Right, KeyModifiers::NONE),
label: "Right",
description: "move prompt cursor right",
},
KeyBinding {
action: KeyCommand::MovePromptCursorUp,
context: KeyContext::Prompt,
key: KeyPattern::new(KeyCode::Up, KeyModifiers::NONE),
label: "Up",
description: "move prompt cursor up",
},
KeyBinding {
action: KeyCommand::MovePromptCursorDown,
context: KeyContext::Prompt,
key: KeyPattern::new(KeyCode::Down, KeyModifiers::NONE),
label: "Down",
description: "move prompt cursor down",
},
KeyBinding {
action: KeyCommand::UndoPrompt,
context: KeyContext::Prompt,
key: KeyPattern::new(KeyCode::Char('z'), KeyModifiers::CONTROL),
label: "Ctrl-Z",
description: "undo prompt edit",
},
KeyBinding {
action: KeyCommand::RedoPrompt,
context: KeyContext::Prompt,
key: KeyPattern::new(KeyCode::Char('y'), KeyModifiers::CONTROL),
label: "Ctrl-Y",
description: "redo prompt edit",
},
KeyBinding {
action: KeyCommand::MovePromptWordLeft,
context: KeyContext::Prompt,
key: KeyPattern::new(KeyCode::Left, KeyModifiers::CONTROL),
label: "Ctrl-Left",
description: "move prompt cursor left by word",
},
KeyBinding {
action: KeyCommand::MovePromptWordRight,
context: KeyContext::Prompt,
key: KeyPattern::new(KeyCode::Right, KeyModifiers::CONTROL),
label: "Ctrl-Right",
description: "move prompt cursor right by word",
},
KeyBinding {
action: KeyCommand::DeletePromptWord,
context: KeyContext::Prompt,
key: KeyPattern::new(KeyCode::Backspace, KeyModifiers::CONTROL),
label: "Ctrl-Backspace",
description: "delete previous prompt word",
},
KeyBinding {
action: KeyCommand::DeletePromptNextWord,
context: KeyContext::Prompt,
key: KeyPattern::new(KeyCode::Delete, KeyModifiers::CONTROL),
label: "Ctrl-Delete",
description: "delete next prompt word",
},
KeyBinding {
action: KeyCommand::ExtendPromptSelectionLeft,
context: KeyContext::Prompt,
key: KeyPattern::new(KeyCode::Left, KeyModifiers::SHIFT),
label: "Shift-Left",
description: "extend prompt selection left",
},
KeyBinding {
action: KeyCommand::ExtendPromptSelectionRight,
context: KeyContext::Prompt,
key: KeyPattern::new(KeyCode::Right, KeyModifiers::SHIFT),
label: "Shift-Right",
description: "extend prompt selection right",
},
KeyBinding {
action: KeyCommand::ExtendPromptSelectionUp,
context: KeyContext::Prompt,
key: KeyPattern::new(KeyCode::Up, KeyModifiers::SHIFT),
label: "Shift-Up",
description: "extend prompt selection up",
},
KeyBinding {
action: KeyCommand::ExtendPromptSelectionDown,
context: KeyContext::Prompt,
key: KeyPattern::new(KeyCode::Down, KeyModifiers::SHIFT),
label: "Shift-Down",
description: "extend prompt selection down",
},
KeyBinding {
action: KeyCommand::SelectAllPrompt,
context: KeyContext::Prompt,
key: KeyPattern::new(KeyCode::Char('a'), KeyModifiers::CONTROL),
label: "Ctrl-A",
description: "select all prompt text",
},
KeyBinding {
action: KeyCommand::ScrollTranscriptUp,
context: KeyContext::Prompt,
key: KeyPattern::new(KeyCode::Up, KeyModifiers::ALT),
label: "Alt-Up",
description: "scroll transcript up",
},
KeyBinding {
action: KeyCommand::ScrollTranscriptDown,
context: KeyContext::Prompt,
key: KeyPattern::new(KeyCode::Down, KeyModifiers::ALT),
label: "Alt-Down",
description: "scroll transcript down",
},
KeyBinding {
action: KeyCommand::ScrollTranscriptUp,
context: KeyContext::Transcript,
key: KeyPattern::new(KeyCode::Up, KeyModifiers::NONE),
label: "Up",
description: "scroll transcript up",
},
KeyBinding {
action: KeyCommand::ScrollTranscriptDown,
context: KeyContext::Transcript,
key: KeyPattern::new(KeyCode::Down, KeyModifiers::NONE),
label: "Down",
description: "scroll transcript down",
},
KeyBinding {
action: KeyCommand::OpenActivityDetail,
context: KeyContext::ActivityTree,
key: KeyPattern::new(KeyCode::Enter, KeyModifiers::NONE),
label: "Enter",
description: "open activity details",
},
KeyBinding {
action: KeyCommand::MoveSelectionDown,
context: KeyContext::ActivityTree,
key: KeyPattern::new(KeyCode::Char('j'), KeyModifiers::ALT),
label: "Alt-J",
description: "move activity selection down",
},
KeyBinding {
action: KeyCommand::MoveSelectionUp,
context: KeyContext::ActivityTree,
key: KeyPattern::new(KeyCode::Char('k'), KeyModifiers::ALT),
label: "Alt-K",
description: "move activity selection up",
},
KeyBinding {
action: KeyCommand::MoveSelectionUp,
context: KeyContext::ActivityTree,
key: KeyPattern::new(KeyCode::Up, KeyModifiers::NONE),
label: "Up",
description: "move activity selection up",
},
KeyBinding {
action: KeyCommand::MoveSelectionDown,
context: KeyContext::ActivityTree,
key: KeyPattern::new(KeyCode::Down, KeyModifiers::NONE),
label: "Down",
description: "move activity selection down",
},
KeyBinding {
action: KeyCommand::CollapseSelected,
context: KeyContext::ActivityTree,
key: KeyPattern::new(KeyCode::Left, KeyModifiers::NONE),
label: "Left",
description: "collapse selected activity",
},
KeyBinding {
action: KeyCommand::ExpandSelected,
context: KeyContext::ActivityTree,
key: KeyPattern::new(KeyCode::Right, KeyModifiers::NONE),
label: "Right",
description: "expand selected activity",
},
KeyBinding {
action: KeyCommand::CollapseSelected,
context: KeyContext::ActivityTree,
key: KeyPattern::new(KeyCode::Char('h'), KeyModifiers::ALT),
label: "Alt-H",
description: "collapse selected activity",
},
KeyBinding {
action: KeyCommand::ExpandSelected,
context: KeyContext::ActivityTree,
key: KeyPattern::new(KeyCode::Char('l'), KeyModifiers::ALT),
label: "Alt-L",
description: "expand selected activity",
},
KeyBinding {
action: KeyCommand::ExpandAll,
context: KeyContext::ActivityTree,
key: KeyPattern::new(KeyCode::Char('e'), KeyModifiers::ALT),
label: "Alt-E",
description: "expand all activities",
},
KeyBinding {
action: KeyCommand::CollapseAll,
context: KeyContext::ActivityTree,
key: KeyPattern::new(KeyCode::Char('c'), KeyModifiers::ALT),
label: "Alt-C",
description: "collapse all activities",
},
KeyBinding {
action: KeyCommand::SelectFailed,
context: KeyContext::ActivityTree,
key: KeyPattern::new(KeyCode::Char('f'), KeyModifiers::ALT),
label: "Alt-F",
description: "select first failed activity",
},
KeyBinding {
action: KeyCommand::SelectRunning,
context: KeyContext::ActivityTree,
key: KeyPattern::new(KeyCode::Char('r'), KeyModifiers::ALT),
label: "Alt-R",
description: "select first running activity",
},
KeyBinding {
action: KeyCommand::ScrollDetailUp,
context: KeyContext::ActivityDetail,
key: KeyPattern::new(KeyCode::Up, KeyModifiers::NONE),
label: "Up",
description: "scroll selected activity detail up",
},
KeyBinding {
action: KeyCommand::ScrollDetailDown,
context: KeyContext::ActivityDetail,
key: KeyPattern::new(KeyCode::Down, KeyModifiers::NONE),
label: "Down",
description: "scroll selected activity detail down",
},
KeyBinding {
action: KeyCommand::PageDetailUp,
context: KeyContext::ActivityDetail,
key: KeyPattern::new(KeyCode::PageUp, KeyModifiers::NONE),
label: "PageUp",
description: "scroll selected activity detail up one page",
},
KeyBinding {
action: KeyCommand::PageDetailDown,
context: KeyContext::ActivityDetail,
key: KeyPattern::new(KeyCode::PageDown, KeyModifiers::NONE),
label: "PageDown",
description: "scroll selected activity detail down one page",
},
KeyBinding {
action: KeyCommand::ScrollDetailUp,
context: KeyContext::ActivityDetail,
key: KeyPattern::new(KeyCode::Up, KeyModifiers::ALT),
label: "Alt-Up",
description: "scroll selected activity detail up",
},
KeyBinding {
action: KeyCommand::ScrollDetailDown,
context: KeyContext::ActivityDetail,
key: KeyPattern::new(KeyCode::Down, KeyModifiers::ALT),
label: "Alt-Down",
description: "scroll selected activity detail down",
},
];
fn bracketed_label(label: &str) -> String {
format!("[{label}]")
}
static HELP_TEXT: LazyLock<String> = LazyLock::new(build_help_text);
pub(super) fn help_text() -> &'static str {
&HELP_TEXT
}
fn build_help_text() -> String {
let mut lines = vec![
"Mission Control Help".to_string(),
"".to_string(),
"Focus: [Tab] cycles Prompt, Transcript or the Changes columns. Unpinned header modals stay outside the Prompt/Transcript cycle. When pinned, [Tab] cycles Prompt, Transcript, then open modals in opening order. [Shift-Tab] switches Transcript/Changes without closing header modals.".to_string(),
"Prompt: type normally when focused; [Enter] submits; [Shift-Enter] inserts a newline; arrows move the cursor; [Ctrl-Z]/[Ctrl-Y] undo/redo; [Ctrl-Left]/[Ctrl-Right] move by word; [Ctrl-Backspace]/[Ctrl-Delete] delete by word; [Shift-Arrow] selects; [Ctrl-A] selects all.".to_string(),
"Newline compatibility: [Ctrl-J] is the portable fallback; [Ctrl-Enter] is another newline shortcut where supported. Some terminals send [Shift-Enter] and [Ctrl-Enter] as plain [Enter], which submits instead.".to_string(),
"Running prompt: [Alt-C] restores queued steering to the editor, joined by newlines; with none queued, it cancels the run.".to_string(),
"Transcript: [Alt-Up]/[Alt-Down] scroll transcript while Prompt is focused; mouse wheel over Transcript also scrolls transcript.".to_string(),
"Activity Tree: click a row or press [Enter] to open details; [Up]/[Down] move selection; [Left]/[Right] collapse/expand; [Alt-E]/[Alt-C] expand/collapse all; [Alt-F]/[Alt-R] select failed/running. [Ctrl-T] opens Activity Tree; [Esc] closes it only when unpinned.".to_string(),
"Selected Activity Detail modal: [Up]/[Down]/[PageUp]/[PageDown] or mouse wheel scroll detail; [Esc] closes and returns to the tree.".to_string(),
"Long prompt: use cursor keys to navigate; the prompt does not scroll with the mouse wheel.".to_string(),
"Header: Panels shows [Alt-1] Activities, [Alt-2] Files, [Alt-3] Summary; a filled dot marks each open panel. Open modals stack in opening order and share height equally. Closing one expands the others; reopening adds it at the bottom. Hide shortcuts stay visible regardless of focus or pinning. When unpinned, [Esc] closes the focused modal, or the last opened when no modal has focus, after dismissing autocomplete. Letters, numbers, or / focus Prompt; editing, paste, and submission keep modals open. Click a rail tab or press [Shift-Tab] to switch views.".to_string(),
"Pin: [Alt-A] or the header Pin toggle pins/unpins all column modals. Pinning with none open opens all three; unpinning then closes them. Otherwise unpinning restores overlays. Pinned modals reserve a column beside Transcript and hide on Changes without closing. They ignore [Esc]. Other shortcut hints and focused border color follow modal focus. Closing the last modal turns pin off. Open panels, their order, and pin behavior survive session switches and save globally for restart. Startup focuses Prompt; focus and scroll are not saved.".to_string(),
"Help: [F1] toggles this overlay; mouse wheel over Help scrolls this text.".to_string(),
"Autocomplete: first-character / shows supported TUI slash commands; @ anywhere tags cwd files with fuzzy matching; $ anywhere tags enabled skills with fuzzy matching; [Up]/[Down] navigate; [Tab]/[Enter] accept only when suggestions are visible; [Esc] dismiss.".to_string(),
"System prompt modal: [Esc]/[q] close; [Up]/[Down]/[Alt-Up]/[Alt-Down]/[PageUp]/[PageDown]/[Home]/[End] and mouse wheel scroll.".to_string(),
"Sessions modal: two columns when space permits; [Tab]/[Shift-Tab] focus list or preview; [Up]/[Down] moves list or scrolls preview; mouse wheel scrolls hovered column; [Enter] switches; [Esc] closes. [Shift-Tab] depends on terminal delivery.".to_string(),
"TUI slash commands: /help opens this overlay; /theme opens the runtime theme picker; /login openai-codex authenticates; /logout openai-codex removes local auth; /model selects cached models; /settings manages settings and refreshes model catalogs; /system-prompt opens read-only prompt inspection; /skills manages skills; /sessions opens the preview session switcher; /new starts a fresh session and clears transcript/activity/detail/help scroll; /quit exits.".to_string(),
"Mouse: wheel follows the hovered scrollable pane; the Prompt editor is not mouse-scrollable; click activity items to select, click selected parents to expand/collapse.".to_string(),
"".to_string(),
];
for context in [
KeyContext::Global,
KeyContext::Prompt,
KeyContext::Transcript,
KeyContext::ActivityTree,
KeyContext::ActivityDetail,
KeyContext::Summary,
] {
lines.push(format!("{context:?}"));
for binding in DEFAULT_KEYBINDINGS
.iter()
.filter(|binding| binding.context == context)
{
lines.push(format!(
" {:<18} {}",
bracketed_label(binding.label),
binding.description
));
}
lines.push(String::new());
}
lines.join("\n")
}
pub(super) fn activity_detail_action_for_key(key: KeyEvent) -> Option<KeyCommand> {
DEFAULT_KEYBINDINGS
.iter()
.find(|binding| binding.context == KeyContext::ActivityDetail && binding.key.matches(key))
.map(|binding| binding.action)
}
pub(super) fn action_for_key(key: KeyEvent, active_context: KeyContext) -> Option<KeyCommand> {
DEFAULT_KEYBINDINGS
.iter()
.find(|binding| {
(binding.context == KeyContext::Global || binding.context == active_context)
&& binding.key.matches(key)
})
.map(|binding| binding.action)
}