mod binding_commands;
mod classify;
mod keys;
mod viewports;
use classify::classify_key_inner_at;
use classify::is_quit_prompt;
use classify::next_primary_agent_id;
pub(crate) use viewports::ActivityTreeViewport;
use viewports::DEFAULT_ACTIVITY_TREE_VIEWPORT;
use viewports::DEFAULT_DETAIL_VIEWPORT;
pub(crate) use viewports::DEFAULT_PICKER_VISIBLE_ROWS;
use viewports::DEFAULT_PROMPT_VIEWPORT;
use viewports::DEFAULT_SYSTEM_PROMPT_VIEWPORT;
use viewports::DEFAULT_TRANSCRIPT_VIEWPORT;
pub(crate) use viewports::DetailViewport;
pub(crate) use viewports::KeyViewports;
pub(crate) use viewports::PickerViewport;
pub(crate) use viewports::PromptViewport;
pub(crate) use viewports::SessionPickerViewport;
pub(crate) use viewports::SystemPromptViewport;
pub(crate) use viewports::TranscriptViewport;
pub(super) use keys::char_autofocuses_prompt;
use keys::is_standalone_ctrl_key;
pub(super) use keys::printable_char_for_text_input;
use keys::shifted_enter_modifiers;
pub(crate) use keys::text_char_for_key_burst;
use binding_commands::classify_keybinding_action;
pub(in crate::tui) mod autocomplete_sources;
pub(in crate::tui) mod clipboard;
mod keybindings;
mod modal;
pub(in crate::tui) mod mouse;
pub(in crate::tui) mod submit_command;
pub(crate) use keybindings::{REWIND_MODE_HELP, THEME_VARIANT_HELP, is_main_tab_cycle_key};
use self::keybindings::{
KeyCommand, KeyContext, action_for_key, help_text as keybindings_help_text,
};
use super::{
autocomplete,
autocomplete::AutocompleteCandidate,
diff, state,
state::{MissionControlState, TuiFocusPane},
};
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers, ModifierKeyCode};
use ratatui::layout::Rect;
use std::{fmt, time::Instant};
#[derive(Clone, PartialEq, Eq)]
pub(crate) struct RedactedInputText(String);
impl RedactedInputText {
pub(crate) fn new(text: String) -> Self {
Self(text)
}
pub(crate) fn into_inner(self) -> String {
self.0
}
}
impl fmt::Debug for RedactedInputText {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("<redacted>")
}
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum InputAction {
None,
SubagentPromptKey(KeyEvent, PromptViewport),
SubagentPromptPaste(RedactedInputText, PromptViewport),
CancelSelectedSubagent,
SettingsEditorKey(crossterm::event::KeyEvent),
SettingsEditorMouse(crossterm::event::MouseEvent, ratatui::layout::Rect),
SettingsEditorPaste(RedactedInputText),
Submit(String),
SelectModel(String),
SelectSession(String),
PreviewTheme(String),
ConfirmTheme,
CancelTheme,
SetSkillEnabled { name: String, enabled: bool },
ToggleSkillsSettingsScope,
SetMcpServerEnabled { name: String, enabled: bool },
SetToolEnabled { name: String, enabled: bool },
ToggleToolsSettingsScope,
SetSubagentProfileEnabled { id: String, enabled: bool },
ToggleSubagentsSettingsScope,
PrimaryAgentSelectionChanged(Option<String>),
ThinkingLevelSelectionChanged(crate::thinking::ThinkingLevel),
SelectConnectProvider(String),
SelectLogoutProvider(String),
MoveConnectProviderUp(usize),
MoveConnectProviderDown(usize),
FocusNextConnectProviderField,
FocusPreviousConnectProviderField,
InsertConnectProviderChar(char),
InsertConnectProviderText(RedactedInputText),
BackspaceConnectProviderField,
DeleteConnectProviderField,
MoveConnectProviderFieldLeft,
MoveConnectProviderFieldRight,
MoveConnectProviderFieldHome,
MoveConnectProviderFieldEnd,
SubmitOAuthFallback,
SubmitConnectProviderStep,
CopyOAuthUrl,
ScrollOAuthUrl(i32),
CopySelectedPrompt(String),
OpenOAuthUrl,
CancelConnectProvider,
RetryConnectProvider,
BackConnectProvider,
CloseConnectProvider,
ResolveBashApproval(bool),
ConfirmLogout(String),
CancelLogout,
ConfirmConnectProviderReplacement,
CancelConnectProviderReplacement,
ConfirmRewind,
CancelRewind,
CancelRunningPrompt,
CancelStartupPrompt,
OpenModel,
Exit,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum InputCommand {
None,
Display(DisplayCommand),
Action(InputAction),
ActionWithDisplay {
action: InputAction,
display: DisplayCommand,
},
}
impl InputCommand {
pub(crate) fn display(display: DisplayCommand) -> Self {
Self::Display(display)
}
pub(crate) fn action(action: InputAction) -> Self {
if matches!(action, InputAction::None) {
Self::None
} else {
Self::Action(action)
}
}
pub(crate) fn action_with_display(action: InputAction, display: DisplayCommand) -> Self {
if matches!(action, InputAction::None) {
Self::Display(display)
} else {
Self::ActionWithDisplay { action, display }
}
}
pub(crate) fn into_parts(self) -> (InputAction, Option<DisplayCommand>) {
match self {
Self::None => (InputAction::None, None),
Self::Display(display) => (InputAction::None, Some(display)),
Self::Action(action) => (action, None),
Self::ActionWithDisplay { action, display } => (action, Some(display)),
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum DisplayCommand {
Diff(super::diff::DiffCommand),
Batch(Vec<DisplayCommand>),
ClearStandaloneCtrl {
suppress_release: bool,
},
StandaloneCtrl {
kind: KeyEventKind,
now: Instant,
},
Focus(FocusCommand),
Prompt(PromptCommand),
Activity(ActivityCommand),
Scroll(ScrollCommand),
ToggleSummary,
ToggleSummaryModal,
ToggleActivityTreeModal,
ToggleSessionFilesModal,
ToggleHeaderModalsPinned,
OpenActivityDetail,
CloseActivityDetail,
ScrollSummary {
files: bool,
down: bool,
rows: u16,
width: u16,
height: u16,
},
Modal(ModalCommand),
Mouse(MouseCommand),
SetPrimaryAgent(Option<String>),
SetThinkingLevel(crate::thinking::ThinkingLevel),
RefreshAutocomplete,
SetStatus(String),
}
impl DisplayCommand {
pub(crate) fn batch(commands: impl IntoIterator<Item = DisplayCommand>) -> Self {
Self::Batch(commands.into_iter().collect())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FocusCommand {
Prompt { close_help: bool },
RightColumnTab(crate::tui::state::RightColumnTab),
NextPrimaryPane,
PreviousPrimaryPane,
ActivityTree,
HeaderModal(super::state::ModalKind),
ToggleHelp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PromptEditCommand {
InsertChar(char),
InsertNewline,
Backspace,
Delete,
MoveCursorLeft,
MoveCursorRight,
MoveCursorUp,
MoveCursorDown,
MoveWordLeft,
MoveWordRight,
Undo,
Redo,
DeleteWord,
DeleteNextWord,
ExtendSelectionLeft,
ExtendSelectionRight,
ExtendSelectionUp,
ExtendSelectionDown,
SelectAll,
Clear,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum PromptCommand {
Edit {
operation: PromptEditCommand,
viewport: PromptViewport,
refresh_autocomplete: bool,
},
InsertText {
text: String,
viewport: PromptViewport,
refresh_autocomplete: bool,
},
TakeForSubmit,
AcceptAutocomplete {
viewport: PromptViewport,
},
MoveAutocompleteUp,
MoveAutocompleteDown,
DismissAutocomplete,
AcceptPromptHistory {
viewport: PromptViewport,
},
DismissPromptHistory,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ActivityCommand {
MoveUp {
visible_rows: u16,
},
MoveDown {
visible_rows: u16,
},
CollapseSelected {
visible_rows: u16,
},
ExpandSelected {
visible_rows: u16,
},
ExpandAll {
visible_rows: u16,
},
CollapseAll {
visible_rows: u16,
},
SelectStatus {
status: crate::output::ActivityStatus,
visible_rows: u16,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ScrollTarget {
Transcript,
ReleaseNotes,
ActivityTree,
Detail,
Help,
SystemPrompt,
Rewind,
Usage,
SessionPreview,
SubagentViewer,
Skills,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ScrollCommand {
pub(crate) target: ScrollTarget,
pub(crate) down: bool,
pub(crate) rows: u16,
pub(crate) visible_rows: u16,
pub(crate) wrap_width: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SettingsModalKind {
Skills,
Mcp,
Tools,
Subagents,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum ModalCommand {
SessionPickerFocusNext,
SessionPickerFocusPrevious,
SessionPickerMove {
focus: crate::tui::state::SessionPickerFocus,
down: bool,
amount: usize,
visible_rows: usize,
preview_rows: u16,
preview_width: u16,
},
CloseSessionPicker,
CloseSubagentViewer,
SubagentViewerSelectNext,
SubagentViewerSelectPrevious,
SubagentViewerScrollHome,
SubagentViewerScrollEnd {
viewport: SystemPromptViewport,
},
ThemeMove {
amount: usize,
down: bool,
visible_rows: usize,
},
ThemeSwitchVariant,
ThemeInsertChar(char),
ThemeBackspace,
ThemeDelete,
ThemeCursorLeft,
ThemeCursorRight,
ThemeCursorHome,
ThemeCursorEnd,
MoveSettings {
kind: SettingsModalKind,
down: bool,
visible_rows: usize,
},
SetSettingRow {
kind: SettingsModalKind,
key: String,
enabled: bool,
},
CloseSettings(SettingsModalKind),
CloseSystemPrompt,
CloseUsage,
RewindPickerMoveOrScroll {
down: bool,
viewport: SystemPromptViewport,
},
CycleRewindMode,
CloseRewind,
CloseLogoutConfirmation,
CloseLogoutPicker,
LogoutPickerMove {
down: bool,
visible_rows: usize,
},
CloseModelPicker,
ModelPickerMove {
down: bool,
visible_rows: usize,
},
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum MouseCommand {
Consume,
ClearSelection,
StartPromptSelection {
byte: usize,
},
UpdatePromptSelection {
byte: usize,
},
FinishPromptSelection {
byte: Option<usize>,
visible_rows: u16,
wrap_width: u16,
},
StartTranscriptSelection {
position: crate::tui::selection::TextPosition,
},
UpdateTranscriptSelection {
position: crate::tui::selection::TextPosition,
},
FinishTranscriptSelection {
wrap_width: u16,
},
ClickActivityTree {
row: u16,
visible_rows: u16,
},
ClickActivityDetail {
row: u16,
visible_rows: u16,
wrap_width: u16,
},
SelectSessionRow {
visible_index: usize,
visible_rows: usize,
},
PinSubagentCard {
batch_id: crate::output::ActivityId,
task_id: crate::output::ActivityId,
},
OpenSubagentViewer {
batch_id: crate::output::ActivityId,
task_id: crate::output::ActivityId,
},
SelectSubagentViewerTab {
visible_index: usize,
},
}
pub(crate) fn help_text() -> &'static str {
keybindings_help_text()
}
pub(crate) fn classify_key_with_all_viewports(
key: KeyEvent,
state: &MissionControlState,
viewports: KeyViewports,
autocomplete_candidates: &[AutocompleteCandidate],
) -> InputCommand {
classify_key_inner_at(
key,
state,
viewports,
autocomplete_candidates,
Instant::now(),
)
}
pub(crate) fn classify_paste_with_all_viewports(
text: &str,
state: &MissionControlState,
viewports: KeyViewports,
_autocomplete_candidates: &[AutocompleteCandidate],
) -> InputCommand {
if text.is_empty() {
return InputCommand::None;
}
if state.top_modal().is_none() && state.diff.dialog.is_some() {
return InputCommand::display(DisplayCommand::Diff(super::diff::DiffCommand::Paste(
text.to_string(),
)));
}
if state.top_modal() == Some(super::state::ModalKind::SubagentViewer) {
return if state.subagent_prompt_editable()
&& let Some(viewport) = viewports.subagent_prompt
{
InputCommand::action(InputAction::SubagentPromptPaste(
RedactedInputText::new(text.to_string()),
viewport,
))
} else {
InputCommand::None
};
}
if state.modals.settings_editor.is_some() {
return InputCommand::action(InputAction::SettingsEditorPaste(RedactedInputText::new(
text.to_string(),
)));
}
let viewport = viewports.prompt.unwrap_or(DEFAULT_PROMPT_VIEWPORT).safe();
if modal_field_accepts_paste(state) {
return InputCommand::action(InputAction::InsertConnectProviderText(
RedactedInputText::new(text.to_string()),
));
}
if prompt_accepts_paste(state) {
return InputCommand::display(DisplayCommand::Prompt(PromptCommand::InsertText {
text: text.to_string(),
viewport,
refresh_autocomplete: true,
}));
}
InputCommand::None
}
pub(crate) fn classify_oversized_paste(state: &MissionControlState) -> InputCommand {
if modal_field_accepts_paste(state) {
InputCommand::display(DisplayCommand::SetStatus(
"pasted modal input is too large".to_string(),
))
} else if input_accepts_paste(state) {
InputCommand::display(DisplayCommand::SetStatus(
super::state::PROMPT_TOO_LARGE_STATUS.to_string(),
))
} else {
InputCommand::None
}
}
pub(crate) fn modal_field_accepts_paste(state: &MissionControlState) -> bool {
if let Some(editor) = &state.modals.settings_editor {
return !editor.busy && editor.editing.is_some();
}
state
.modals
.connect_provider
.as_ref()
.is_some_and(|connect| {
matches!(
connect.stage,
crate::tui::state::ConnectProviderStage::ConfigureCustom
| crate::tui::state::ConnectProviderStage::OAuth
)
})
}
pub(crate) fn input_accepts_paste(state: &MissionControlState) -> bool {
if state.top_modal() == Some(super::state::ModalKind::SubagentViewer) {
return state.subagent_prompt_editable();
}
modal_field_accepts_paste(state) || prompt_accepts_paste(state)
}
pub(crate) fn prompt_accepts_paste(state: &MissionControlState) -> bool {
state.prompt_accepts_input()
}