use super::{
PickerViewport, SessionPickerViewport, SystemPromptViewport, printable_char_for_text_input,
};
use crate::tui::state::{ConnectProviderStage, MissionControlState, ModalKind};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::layout::Rect;
#[derive(Debug, Clone, Copy)]
pub(super) struct ModalKeyViewports {
pub(super) system_prompt: SystemPromptViewport,
pub(super) rewind_modal: SystemPromptViewport,
pub(super) usage_modal: SystemPromptViewport,
pub(super) connect_provider: Option<PickerViewport>,
pub(super) connect_provider_modal_visible: bool,
pub(super) connect_provider_custom_field_visible: bool,
pub(super) connect_provider_oauth_controls_visible: bool,
pub(super) connect_provider_oauth_fallback_visible: bool,
pub(super) logout_picker: Option<PickerViewport>,
pub(super) logout_confirmation_action_row: Option<Rect>,
pub(super) model_picker: Option<PickerViewport>,
pub(super) sessions_modal: Option<SessionPickerViewport>,
pub(super) subagent_viewer: SystemPromptViewport,
pub(super) skills_modal: Option<PickerViewport>,
pub(super) skills_scope_visible: bool,
pub(super) mcp_modal: Option<PickerViewport>,
pub(super) tools_modal: Option<PickerViewport>,
pub(super) tools_scope_visible: bool,
pub(super) subagents_modal: Option<PickerViewport>,
pub(super) subagents_scope_visible: bool,
pub(super) models_modal: Option<PickerViewport>,
pub(super) models_scope_visible: bool,
pub(super) theme_picker: Option<PickerViewport>,
pub(super) theme_query_visible: bool,
}
pub(super) fn classify_modal_key(
key: KeyEvent,
state: &MissionControlState,
viewports: ModalKeyViewports,
) -> Option<super::InputCommand> {
match state.top_modal()? {
ModalKind::SubagentViewer => {
Some(classify_subagent_viewer_key(key, viewports.subagent_viewer))
}
ModalKind::SessionPicker => Some(classify_session_picker_key(
key,
state,
viewports.sessions_modal,
)),
ModalKind::Skills => Some(classify_settings_modal_key(
key,
state,
super::SettingsModalKind::Skills,
viewports.skills_modal,
viewports.skills_scope_visible,
)),
ModalKind::Mcp => Some(classify_settings_modal_key(
key,
state,
super::SettingsModalKind::Mcp,
viewports.mcp_modal,
false,
)),
ModalKind::Tools => Some(classify_settings_modal_key(
key,
state,
super::SettingsModalKind::Tools,
viewports.tools_modal,
viewports.tools_scope_visible,
)),
ModalKind::Subagents => Some(classify_settings_modal_key(
key,
state,
super::SettingsModalKind::Subagents,
viewports.subagents_modal,
viewports.subagents_scope_visible,
)),
ModalKind::Models => Some(classify_settings_modal_key(
key,
state,
super::SettingsModalKind::Models,
viewports.models_modal,
viewports.models_scope_visible,
)),
ModalKind::Usage => Some(classify_system_scroll_modal_key(
key,
super::ScrollTarget::Usage,
viewports.usage_modal,
)),
ModalKind::SystemPrompt => Some(classify_system_scroll_modal_key(
key,
super::ScrollTarget::SystemPrompt,
viewports.system_prompt,
)),
ModalKind::Rewind => Some(classify_rewind_modal_key(
key,
state,
viewports.rewind_modal,
)),
ModalKind::ConnectProvider => Some(handle_connect_provider_key(
key,
state,
viewports.connect_provider,
viewports.connect_provider_modal_visible,
viewports.connect_provider_custom_field_visible,
viewports.connect_provider_oauth_controls_visible,
viewports.connect_provider_oauth_fallback_visible,
)),
ModalKind::LogoutConfirmation => Some(classify_logout_confirmation_key(
key,
state,
viewports.logout_confirmation_action_row,
)),
ModalKind::LogoutPicker => Some(classify_logout_picker_key(
key,
state,
viewports.logout_picker,
)),
ModalKind::ModelPicker => Some(classify_model_picker_key(
key,
state,
viewports.model_picker,
)),
ModalKind::ThemePicker => Some(classify_theme_picker_key(
key,
state,
viewports.theme_picker,
viewports.theme_query_visible,
)),
}
}
fn handle_connect_provider_key(
key: KeyEvent,
state: &MissionControlState,
viewport: Option<PickerViewport>,
modal_visible: bool,
custom_field_visible: bool,
oauth_controls_visible: bool,
oauth_fallback_visible: bool,
) -> super::InputCommand {
let Some(stage) = state
.modals
.connect_provider
.as_ref()
.map(|connect| connect.stage)
else {
return super::InputCommand::None;
};
let action = match stage {
ConnectProviderStage::ChooseProvider => match (key.code, key.modifiers) {
(KeyCode::Up, KeyModifiers::NONE) => {
viewport.map_or(super::InputAction::None, |viewport| {
super::InputAction::MoveConnectProviderUp(viewport.visible_rows as usize)
})
}
(KeyCode::Down, KeyModifiers::NONE) => {
viewport.map_or(super::InputAction::None, |viewport| {
super::InputAction::MoveConnectProviderDown(viewport.visible_rows as usize)
})
}
(KeyCode::Enter, KeyModifiers::NONE) if viewport.is_some() => {
state.selected_connect_provider_id().map_or(
super::InputAction::None,
super::InputAction::SelectConnectProvider,
)
}
(KeyCode::Esc, KeyModifiers::NONE) => super::InputAction::CancelConnectProvider,
_ => super::InputAction::None,
},
ConnectProviderStage::ConfigureCustom => match (key.code, key.modifiers) {
(KeyCode::Tab, KeyModifiers::NONE) if custom_field_visible => {
super::InputAction::FocusNextConnectProviderField
}
(KeyCode::BackTab, modifiers)
if custom_field_visible
&& (modifiers.is_empty() || modifiers == KeyModifiers::SHIFT) =>
{
super::InputAction::FocusPreviousConnectProviderField
}
(KeyCode::Enter, KeyModifiers::NONE) if custom_field_visible => {
super::InputAction::SubmitConnectProviderStep
}
(KeyCode::Esc, KeyModifiers::NONE) => super::InputAction::CancelConnectProvider,
(KeyCode::Backspace, KeyModifiers::NONE) if custom_field_visible => {
super::InputAction::BackspaceConnectProviderField
}
(KeyCode::Delete, KeyModifiers::NONE) if custom_field_visible => {
super::InputAction::DeleteConnectProviderField
}
(KeyCode::Left, KeyModifiers::NONE) if custom_field_visible => {
super::InputAction::MoveConnectProviderFieldLeft
}
(KeyCode::Right, KeyModifiers::NONE) if custom_field_visible => {
super::InputAction::MoveConnectProviderFieldRight
}
(KeyCode::Home, KeyModifiers::NONE) if custom_field_visible => {
super::InputAction::MoveConnectProviderFieldHome
}
(KeyCode::End, KeyModifiers::NONE) if custom_field_visible => {
super::InputAction::MoveConnectProviderFieldEnd
}
(KeyCode::Char(_), _) if custom_field_visible => printable_char_for_text_input(key)
.map_or(
super::InputAction::None,
super::InputAction::InsertConnectProviderChar,
),
_ => super::InputAction::None,
},
ConnectProviderStage::AnthropicGuidance => match (key.code, key.modifiers) {
(KeyCode::Enter, KeyModifiers::NONE) if modal_visible => {
super::InputAction::BackConnectProvider
}
(KeyCode::Esc, KeyModifiers::NONE) => super::InputAction::CancelConnectProvider,
_ => super::InputAction::None,
},
ConnectProviderStage::OAuth => match (key.code, key.modifiers) {
(KeyCode::Esc, KeyModifiers::NONE) => super::InputAction::CancelConnectProvider,
(KeyCode::Char('y'), KeyModifiers::CONTROL) if oauth_controls_visible => {
super::InputAction::CopyOAuthUrl
}
(KeyCode::Char('o'), KeyModifiers::CONTROL) if oauth_controls_visible => {
super::InputAction::OpenOAuthUrl
}
(KeyCode::Enter, KeyModifiers::NONE) if oauth_fallback_visible => state
.modals
.connect_provider
.as_ref()
.filter(|connect| !connect.fallback_input.is_empty())
.map_or(super::InputAction::None, |_| {
super::InputAction::SubmitOAuthFallback
}),
(KeyCode::Backspace, KeyModifiers::NONE) if oauth_fallback_visible => {
super::InputAction::BackspaceConnectProviderField
}
(KeyCode::Delete, KeyModifiers::NONE) if oauth_fallback_visible => {
super::InputAction::DeleteConnectProviderField
}
(KeyCode::Left, KeyModifiers::NONE) if oauth_fallback_visible => {
super::InputAction::MoveConnectProviderFieldLeft
}
(KeyCode::Right, KeyModifiers::NONE) if oauth_fallback_visible => {
super::InputAction::MoveConnectProviderFieldRight
}
(KeyCode::Home, KeyModifiers::NONE) if oauth_fallback_visible => {
super::InputAction::MoveConnectProviderFieldHome
}
(KeyCode::End, KeyModifiers::NONE) if oauth_fallback_visible => {
super::InputAction::MoveConnectProviderFieldEnd
}
(KeyCode::Char(_), _) if oauth_fallback_visible => printable_char_for_text_input(key)
.map_or(
super::InputAction::None,
super::InputAction::InsertConnectProviderChar,
),
_ => super::InputAction::None,
},
ConnectProviderStage::ReplacementConfirmation => match (key.code, key.modifiers) {
(KeyCode::Enter, KeyModifiers::NONE) if modal_visible => {
super::InputAction::ConfirmConnectProviderReplacement
}
(KeyCode::Esc, KeyModifiers::NONE) => {
super::InputAction::CancelConnectProviderReplacement
}
_ => super::InputAction::None,
},
ConnectProviderStage::Saving => super::InputAction::None,
ConnectProviderStage::Success => match (key.code, key.modifiers) {
(KeyCode::Enter, KeyModifiers::NONE) if modal_visible => {
super::InputAction::CloseConnectProvider
}
(KeyCode::Esc, KeyModifiers::NONE) => super::InputAction::CloseConnectProvider,
_ => super::InputAction::None,
},
ConnectProviderStage::Error => match (key.code, key.modifiers) {
(KeyCode::Enter, KeyModifiers::NONE) if modal_visible => {
super::InputAction::RetryConnectProvider
}
(KeyCode::Backspace | KeyCode::Left, KeyModifiers::NONE) if modal_visible => {
super::InputAction::BackConnectProvider
}
(KeyCode::Esc, KeyModifiers::NONE) => super::InputAction::CancelConnectProvider,
_ => super::InputAction::None,
},
};
super::InputCommand::action(action)
}
fn classify_theme_picker_key(
key: KeyEvent,
state: &MissionControlState,
viewport: Option<PickerViewport>,
query_visible: bool,
) -> super::InputCommand {
if state.theme_picker_save_pending() {
return super::InputCommand::None;
}
let preview = |id: Option<String>, display: super::DisplayCommand| match id {
Some(id) => {
super::InputCommand::action_with_display(super::InputAction::PreviewTheme(id), display)
}
None => super::InputCommand::display(display),
};
let visible_rows = viewport.map(|viewport| viewport.visible_rows as usize);
match (key.code, key.modifiers) {
(KeyCode::Up, KeyModifiers::NONE) => {
let Some(visible_rows) = visible_rows else {
return super::InputCommand::None;
};
preview(
theme_selected_id_after_move(state, 1, false),
super::DisplayCommand::Modal(super::ModalCommand::ThemeMove {
amount: 1,
down: false,
visible_rows,
}),
)
}
(KeyCode::Down, KeyModifiers::NONE) => {
let Some(visible_rows) = visible_rows else {
return super::InputCommand::None;
};
preview(
theme_selected_id_after_move(state, 1, true),
super::DisplayCommand::Modal(super::ModalCommand::ThemeMove {
amount: 1,
down: true,
visible_rows,
}),
)
}
(KeyCode::PageUp, KeyModifiers::NONE) => {
let Some(visible_rows) = visible_rows else {
return super::InputCommand::None;
};
preview(
theme_selected_id_after_move(state, visible_rows.max(1), false),
super::DisplayCommand::Modal(super::ModalCommand::ThemeMove {
amount: visible_rows.max(1),
down: false,
visible_rows,
}),
)
}
(KeyCode::PageDown, KeyModifiers::NONE) => {
let Some(visible_rows) = visible_rows else {
return super::InputCommand::None;
};
preview(
theme_selected_id_after_move(state, visible_rows.max(1), true),
super::DisplayCommand::Modal(super::ModalCommand::ThemeMove {
amount: visible_rows.max(1),
down: true,
visible_rows,
}),
)
}
(KeyCode::Enter, KeyModifiers::NONE) if viewport.is_some() => {
super::InputCommand::action(super::InputAction::ConfirmTheme)
}
(KeyCode::Esc, KeyModifiers::NONE) => {
super::InputCommand::action(super::InputAction::CancelTheme)
}
(KeyCode::Backspace, KeyModifiers::NONE) if query_visible => preview(
theme_selected_id_after_edit(state, ThemeQueryEdit::Backspace),
super::DisplayCommand::Modal(super::ModalCommand::ThemeBackspace),
),
(KeyCode::Delete, KeyModifiers::NONE) if query_visible => preview(
theme_selected_id_after_edit(state, ThemeQueryEdit::Delete),
super::DisplayCommand::Modal(super::ModalCommand::ThemeDelete),
),
(KeyCode::Left, KeyModifiers::NONE) if query_visible => super::InputCommand::display(
super::DisplayCommand::Modal(super::ModalCommand::ThemeCursorLeft),
),
(KeyCode::Right, KeyModifiers::NONE) if query_visible => super::InputCommand::display(
super::DisplayCommand::Modal(super::ModalCommand::ThemeCursorRight),
),
(KeyCode::Home, KeyModifiers::NONE) if query_visible => super::InputCommand::display(
super::DisplayCommand::Modal(super::ModalCommand::ThemeCursorHome),
),
(KeyCode::End, KeyModifiers::NONE) if query_visible => super::InputCommand::display(
super::DisplayCommand::Modal(super::ModalCommand::ThemeCursorEnd),
),
(KeyCode::Char(ch), modifiers)
if query_visible && (modifiers.is_empty() || modifiers == KeyModifiers::SHIFT) =>
{
preview(
theme_selected_id_after_edit(state, ThemeQueryEdit::Insert(ch)),
super::DisplayCommand::Modal(super::ModalCommand::ThemeInsertChar(ch)),
)
}
_ => super::InputCommand::None,
}
}
fn classify_logout_picker_key(
key: KeyEvent,
state: &MissionControlState,
viewport: Option<PickerViewport>,
) -> super::InputCommand {
let display = |command| super::InputCommand::display(super::DisplayCommand::Modal(command));
match (key.code, key.modifiers) {
(KeyCode::Up, KeyModifiers::NONE) => {
viewport.map_or(super::InputCommand::None, |viewport| {
display(super::ModalCommand::LogoutPickerMove {
down: false,
visible_rows: viewport.visible_rows as usize,
})
})
}
(KeyCode::Down, KeyModifiers::NONE) => {
viewport.map_or(super::InputCommand::None, |viewport| {
display(super::ModalCommand::LogoutPickerMove {
down: true,
visible_rows: viewport.visible_rows as usize,
})
})
}
(KeyCode::Enter, KeyModifiers::NONE) if viewport.is_some() => state
.selected_logout_provider_id()
.map_or(super::InputCommand::None, |provider_id| {
super::InputCommand::action_with_display(
super::InputAction::SelectLogoutProvider(provider_id),
super::DisplayCommand::Modal(super::ModalCommand::CloseLogoutPicker),
)
}),
(KeyCode::Esc, KeyModifiers::NONE) => display(super::ModalCommand::CloseLogoutPicker),
_ => super::InputCommand::None,
}
}
#[derive(Debug, Clone, Copy)]
enum ThemeQueryEdit {
Insert(char),
Backspace,
Delete,
}
fn theme_selected_id_after_move(
state: &MissionControlState,
amount: usize,
down: bool,
) -> Option<String> {
let picker = state.modals.theme_picker.as_ref()?;
if picker.loading || picker.filtered_indices.is_empty() {
return None;
}
let selected = if down {
picker
.selected
.saturating_add(amount)
.min(picker.filtered_indices.len() - 1)
} else {
picker.selected.saturating_sub(amount)
};
picker
.filtered_indices
.get(selected)
.and_then(|index| picker.entries.get(*index))
.map(|entry| entry.id.clone())
}
fn theme_selected_id_after_edit(
state: &MissionControlState,
edit: ThemeQueryEdit,
) -> Option<String> {
use unicode_segmentation::UnicodeSegmentation;
let picker = state.modals.theme_picker.as_ref()?;
if picker.loading {
return None;
}
let mut query = picker.query.clone();
let cursor = picker.query_cursor;
match edit {
ThemeQueryEdit::Insert(ch) => {
query.insert(cursor, ch);
}
ThemeQueryEdit::Backspace => {
if let Some((previous, _)) = query[..cursor].grapheme_indices(true).next_back() {
query.drain(previous..cursor);
}
}
ThemeQueryEdit::Delete => {
if let Some(grapheme) = query[cursor..].graphemes(true).next() {
query.drain(cursor..cursor + grapheme.len());
}
}
}
let selected_id = picker
.filtered_indices
.get(picker.selected)
.and_then(|index| picker.entries.get(*index))
.map(|entry| entry.id.as_str());
let needle = query.to_lowercase();
let filtered_indices = picker
.entries
.iter()
.enumerate()
.filter(|(_, entry)| {
needle.is_empty()
|| entry.id.to_lowercase().contains(&needle)
|| entry.display_name.to_lowercase().contains(&needle)
})
.map(|(index, _)| index)
.collect::<Vec<_>>();
let selected = selected_id
.and_then(|id| {
filtered_indices.iter().position(|index| {
picker
.entries
.get(*index)
.is_some_and(|entry| entry.id == id)
})
})
.unwrap_or(0);
filtered_indices
.get(selected.min(filtered_indices.len().saturating_sub(1)))
.and_then(|index| picker.entries.get(*index))
.map(|entry| entry.id.clone())
}
fn classify_subagent_viewer_key(
key: KeyEvent,
viewport: SystemPromptViewport,
) -> super::InputCommand {
let display = |command| super::InputCommand::display(super::DisplayCommand::Modal(command));
let viewport = viewport.safe();
let scroll = |down, rows| {
super::InputCommand::display(super::DisplayCommand::Scroll(super::ScrollCommand {
target: super::ScrollTarget::SubagentViewer,
down,
rows,
visible_rows: viewport.visible_rows,
wrap_width: viewport.wrap_width,
}))
};
match (key.code, key.modifiers) {
(KeyCode::Esc, KeyModifiers::NONE) => display(super::ModalCommand::CloseSubagentViewer),
(KeyCode::Tab, KeyModifiers::NONE) | (KeyCode::Right, KeyModifiers::NONE) => {
display(super::ModalCommand::SubagentViewerSelectNext)
}
(KeyCode::BackTab, modifiers)
if modifiers.is_empty() || modifiers == KeyModifiers::SHIFT =>
{
display(super::ModalCommand::SubagentViewerSelectPrevious)
}
(KeyCode::Left, KeyModifiers::NONE) => {
display(super::ModalCommand::SubagentViewerSelectPrevious)
}
(KeyCode::Up, KeyModifiers::NONE) => scroll(false, 1),
(KeyCode::Down, KeyModifiers::NONE) => scroll(true, 1),
(KeyCode::PageUp, KeyModifiers::NONE) => scroll(false, viewport.visible_rows.max(1)),
(KeyCode::PageDown, KeyModifiers::NONE) => scroll(true, viewport.visible_rows.max(1)),
(KeyCode::Home, KeyModifiers::NONE) => {
display(super::ModalCommand::SubagentViewerScrollHome)
}
(KeyCode::End, KeyModifiers::NONE) => {
display(super::ModalCommand::SubagentViewerScrollEnd { viewport })
}
_ => super::InputCommand::None,
}
}
fn classify_session_picker_key(
key: KeyEvent,
state: &MissionControlState,
viewport: Option<SessionPickerViewport>,
) -> super::InputCommand {
let display = |command| super::InputCommand::display(super::DisplayCommand::Modal(command));
let Some(viewport) = viewport else {
return match (key.code, key.modifiers) {
(KeyCode::Esc, KeyModifiers::NONE) => display(super::ModalCommand::CloseSessionPicker),
_ => super::InputCommand::None,
};
};
let focus = state
.modals
.session_picker
.as_ref()
.map(|picker| picker.focus)
.unwrap_or(crate::tui::state::SessionPickerFocus::List);
let focus = if viewport.preview_visible {
focus
} else {
crate::tui::state::SessionPickerFocus::List
};
match (key.code, key.modifiers) {
(KeyCode::Tab, KeyModifiers::NONE) if viewport.preview_visible => {
display(super::ModalCommand::SessionPickerFocusNext)
}
(KeyCode::BackTab, modifiers)
if viewport.preview_visible
&& (modifiers.is_empty() || modifiers == KeyModifiers::SHIFT) =>
{
display(super::ModalCommand::SessionPickerFocusPrevious)
}
(KeyCode::Up, KeyModifiers::NONE) => match focus {
crate::tui::state::SessionPickerFocus::List => {
display(super::ModalCommand::SessionPickerMove {
focus,
down: false,
amount: 1,
visible_rows: viewport.list_rows as usize,
preview_rows: viewport.preview_rows,
preview_width: viewport.preview_width,
})
}
crate::tui::state::SessionPickerFocus::Preview => {
super::InputCommand::display(super::DisplayCommand::Scroll(super::ScrollCommand {
target: super::ScrollTarget::SessionPreview,
down: false,
rows: 1,
visible_rows: viewport.preview_rows,
wrap_width: viewport.preview_width,
}))
}
},
(KeyCode::Down, KeyModifiers::NONE) => match focus {
crate::tui::state::SessionPickerFocus::List => {
display(super::ModalCommand::SessionPickerMove {
focus,
down: true,
amount: 1,
visible_rows: viewport.list_rows as usize,
preview_rows: viewport.preview_rows,
preview_width: viewport.preview_width,
})
}
crate::tui::state::SessionPickerFocus::Preview => {
super::InputCommand::display(super::DisplayCommand::Scroll(super::ScrollCommand {
target: super::ScrollTarget::SessionPreview,
down: true,
rows: 1,
visible_rows: viewport.preview_rows,
wrap_width: viewport.preview_width,
}))
}
},
(KeyCode::PageUp, KeyModifiers::NONE) => match focus {
crate::tui::state::SessionPickerFocus::List => {
display(super::ModalCommand::SessionPickerMove {
focus,
down: false,
amount: viewport.list_rows.max(1) as usize,
visible_rows: viewport.list_rows as usize,
preview_rows: viewport.preview_rows,
preview_width: viewport.preview_width,
})
}
crate::tui::state::SessionPickerFocus::Preview => {
super::InputCommand::display(super::DisplayCommand::Scroll(super::ScrollCommand {
target: super::ScrollTarget::SessionPreview,
down: false,
rows: viewport.preview_rows,
visible_rows: viewport.preview_rows,
wrap_width: viewport.preview_width,
}))
}
},
(KeyCode::PageDown, KeyModifiers::NONE) => match focus {
crate::tui::state::SessionPickerFocus::List => {
display(super::ModalCommand::SessionPickerMove {
focus,
down: true,
amount: viewport.list_rows.max(1) as usize,
visible_rows: viewport.list_rows as usize,
preview_rows: viewport.preview_rows,
preview_width: viewport.preview_width,
})
}
crate::tui::state::SessionPickerFocus::Preview => {
super::InputCommand::display(super::DisplayCommand::Scroll(super::ScrollCommand {
target: super::ScrollTarget::SessionPreview,
down: true,
rows: viewport.preview_rows,
visible_rows: viewport.preview_rows,
wrap_width: viewport.preview_width,
}))
}
},
(KeyCode::Enter, KeyModifiers::NONE) => {
state
.selected_session_id()
.map_or(super::InputCommand::None, |session_id| {
super::InputCommand::action_with_display(
super::InputAction::SelectSession(session_id),
super::DisplayCommand::Modal(super::ModalCommand::CloseSessionPicker),
)
})
}
(KeyCode::Esc, KeyModifiers::NONE) => display(super::ModalCommand::CloseSessionPicker),
_ => super::InputCommand::None,
}
}
fn classify_settings_modal_key(
key: KeyEvent,
state: &MissionControlState,
kind: super::SettingsModalKind,
viewport: Option<PickerViewport>,
scope_visible: bool,
) -> super::InputCommand {
let display = |command| super::InputCommand::display(super::DisplayCommand::Modal(command));
match (key.code, key.modifiers) {
(KeyCode::Tab, KeyModifiers::NONE) if scope_visible => {
super::InputCommand::action(match kind {
super::SettingsModalKind::Skills => super::InputAction::ToggleSkillsSettingsScope,
super::SettingsModalKind::Mcp => super::InputAction::None,
super::SettingsModalKind::Tools => super::InputAction::ToggleToolsSettingsScope,
super::SettingsModalKind::Subagents => {
super::InputAction::ToggleSubagentsSettingsScope
}
super::SettingsModalKind::Models => super::InputAction::ToggleModelsSettingsScope,
})
}
(KeyCode::Up, KeyModifiers::NONE) => viewport
.map_or(super::InputCommand::None, |viewport| {
display(settings_move_command(kind, false, viewport))
}),
(KeyCode::Down, KeyModifiers::NONE) => viewport
.map_or(super::InputCommand::None, |viewport| {
display(settings_move_command(kind, true, viewport))
}),
(KeyCode::Enter, KeyModifiers::NONE) => {
if viewport.is_none() {
return super::InputCommand::None;
}
let toggle = match kind {
super::SettingsModalKind::Skills => state
.modals
.skills_modal
.as_ref()
.and_then(|modal| modal.rows.get(modal.selected))
.map(|row| (row.name.clone(), !row.enabled))
.map(|(key, enabled)| {
(
super::InputAction::SetSkillEnabled {
name: key.clone(),
enabled,
},
super::ModalCommand::SetSettingRow { kind, key, enabled },
)
}),
super::SettingsModalKind::Mcp => state
.modals
.mcp_modal
.as_ref()
.and_then(|modal| modal.rows.get(modal.selected))
.map(|row| (row.name.clone(), !row.enabled))
.map(|(key, enabled)| {
(
super::InputAction::SetMcpServerEnabled {
name: key.clone(),
enabled,
},
super::ModalCommand::SetSettingRow { kind, key, enabled },
)
}),
super::SettingsModalKind::Tools => state
.modals
.tools_modal
.as_ref()
.and_then(|modal| modal.rows.get(modal.selected))
.map(|row| (row.name.clone(), !row.enabled))
.map(|(key, enabled)| {
(
super::InputAction::SetToolEnabled {
name: key.clone(),
enabled,
},
super::ModalCommand::SetSettingRow { kind, key, enabled },
)
}),
super::SettingsModalKind::Subagents => state
.modals
.subagents_modal
.as_ref()
.and_then(|modal| modal.rows.get(modal.selected))
.map(|row| (row.id.clone(), !row.enabled))
.map(|(key, enabled)| {
(
super::InputAction::SetSubagentProfileEnabled {
id: key.clone(),
enabled,
},
super::ModalCommand::SetSettingRow { kind, key, enabled },
)
}),
super::SettingsModalKind::Models => state
.modals
.models_modal
.as_ref()
.and_then(|modal| modal.rows.get(modal.selected))
.map(|row| (row.id.clone(), !row.enabled))
.map(|(key, enabled)| {
(
super::InputAction::SetModelEnabled {
id: key.clone(),
enabled,
},
super::ModalCommand::SetSettingRow { kind, key, enabled },
)
}),
};
toggle.map_or(super::InputCommand::None, |(action, command)| {
super::InputCommand::action_with_display(
action,
super::DisplayCommand::Modal(command),
)
})
}
(KeyCode::Esc, KeyModifiers::NONE) => display(super::ModalCommand::CloseSettings(kind)),
_ => super::InputCommand::None,
}
}
fn settings_move_command(
kind: super::SettingsModalKind,
down: bool,
viewport: PickerViewport,
) -> super::ModalCommand {
super::ModalCommand::MoveSettings {
kind,
down,
visible_rows: viewport.visible_rows as usize,
}
}
fn classify_system_scroll_modal_key(
key: KeyEvent,
target: super::ScrollTarget,
viewport: SystemPromptViewport,
) -> super::InputCommand {
let display = |command| super::InputCommand::display(super::DisplayCommand::Modal(command));
let scroll = |down, rows| {
super::InputCommand::display(super::DisplayCommand::Scroll(super::ScrollCommand {
target,
down,
rows,
visible_rows: viewport.visible_rows,
wrap_width: viewport.wrap_width,
}))
};
match (key.code, key.modifiers) {
(KeyCode::Esc | KeyCode::Char('q'), KeyModifiers::NONE) => match target {
super::ScrollTarget::SystemPrompt => display(super::ModalCommand::CloseSystemPrompt),
super::ScrollTarget::Usage => display(super::ModalCommand::CloseUsage),
_ => super::InputCommand::None,
},
(KeyCode::Up, KeyModifiers::NONE | KeyModifiers::ALT) => scroll(false, 1),
(KeyCode::Down, KeyModifiers::NONE | KeyModifiers::ALT) => scroll(true, 1),
(KeyCode::PageUp, KeyModifiers::NONE) => scroll(false, viewport.visible_rows),
(KeyCode::PageDown, KeyModifiers::NONE) => scroll(true, viewport.visible_rows),
(KeyCode::Home, KeyModifiers::NONE) => scroll(false, u16::MAX),
(KeyCode::End, KeyModifiers::NONE) => scroll(true, u16::MAX),
_ => super::InputCommand::None,
}
}
fn classify_rewind_modal_key(
key: KeyEvent,
state: &MissionControlState,
viewport: SystemPromptViewport,
) -> super::InputCommand {
let display = |command| super::InputCommand::display(super::DisplayCommand::Modal(command));
let scroll = |down, rows| {
super::InputCommand::display(super::DisplayCommand::Scroll(super::ScrollCommand {
target: super::ScrollTarget::Rewind,
down,
rows,
visible_rows: viewport.visible_rows,
wrap_width: viewport.wrap_width,
}))
};
match (key.code, key.modifiers) {
(super::keybindings::REWIND_MODE_KEY, KeyModifiers::NONE) => {
display(super::ModalCommand::CycleRewindMode)
}
(KeyCode::Enter, KeyModifiers::NONE) => {
if state.modals.rewind_modal.as_ref().is_some_and(|modal| {
(modal.plan.is_some() && !modal.dry_run) || !modal.plans.is_empty()
}) {
super::InputCommand::action(super::InputAction::ConfirmRewind)
} else {
super::InputCommand::None
}
}
(KeyCode::Esc, KeyModifiers::NONE) => super::InputCommand::action_with_display(
super::InputAction::CancelRewind,
super::DisplayCommand::Modal(super::ModalCommand::CloseRewind),
),
(KeyCode::Up, KeyModifiers::NONE | KeyModifiers::ALT) => {
display(super::ModalCommand::RewindPickerMoveOrScroll {
down: false,
viewport,
})
}
(KeyCode::Down, KeyModifiers::NONE | KeyModifiers::ALT) => {
display(super::ModalCommand::RewindPickerMoveOrScroll {
down: true,
viewport,
})
}
(KeyCode::PageUp, KeyModifiers::NONE) => scroll(false, viewport.visible_rows),
(KeyCode::PageDown, KeyModifiers::NONE) => scroll(true, viewport.visible_rows),
_ => super::InputCommand::None,
}
}
fn classify_logout_confirmation_key(
key: KeyEvent,
state: &MissionControlState,
action_row: Option<Rect>,
) -> super::InputCommand {
match (key.code, key.modifiers) {
(KeyCode::Enter, KeyModifiers::NONE) if action_row.is_some() => state
.modals
.logout_confirmation
.as_ref()
.map(|confirmation| {
super::InputCommand::action_with_display(
super::InputAction::ConfirmLogout(confirmation.provider_id.clone()),
super::DisplayCommand::Modal(super::ModalCommand::CloseLogoutConfirmation),
)
})
.unwrap_or(super::InputCommand::None),
(KeyCode::Esc, KeyModifiers::NONE) => super::InputCommand::action_with_display(
super::InputAction::CancelLogout,
super::DisplayCommand::Modal(super::ModalCommand::CloseLogoutConfirmation),
),
_ => super::InputCommand::None,
}
}
fn classify_model_picker_key(
key: KeyEvent,
state: &MissionControlState,
viewport: Option<PickerViewport>,
) -> super::InputCommand {
let display = |command| super::InputCommand::display(super::DisplayCommand::Modal(command));
match (key.code, key.modifiers) {
(KeyCode::Up, KeyModifiers::NONE) => {
viewport.map_or(super::InputCommand::None, |viewport| {
display(super::ModalCommand::ModelPickerMove {
down: false,
visible_rows: viewport.visible_rows as usize,
})
})
}
(KeyCode::Down, KeyModifiers::NONE) => {
viewport.map_or(super::InputCommand::None, |viewport| {
display(super::ModalCommand::ModelPickerMove {
down: true,
visible_rows: viewport.visible_rows as usize,
})
})
}
(KeyCode::Enter, KeyModifiers::NONE) if viewport.is_some() => state
.selected_model_picker_id()
.map_or(super::InputCommand::None, |model_id| {
super::InputCommand::action_with_display(
super::InputAction::SelectModel(model_id),
super::DisplayCommand::Modal(super::ModalCommand::CloseModelPicker),
)
}),
(KeyCode::Esc, KeyModifiers::NONE) => display(super::ModalCommand::CloseModelPicker),
_ => super::InputCommand::None,
}
}