use super::*;
pub(super) fn classify_key_inner_at(
key: KeyEvent,
state: &MissionControlState,
viewports: KeyViewports,
autocomplete_candidates: &[AutocompleteCandidate],
now: Instant,
) -> InputCommand {
let standalone_ctrl = is_standalone_ctrl_key(key);
if key.kind == KeyEventKind::Release && !standalone_ctrl {
return InputCommand::None;
}
if state.can_switch_main_tabs() && is_main_tab_cycle_key(key) {
let tab = match state.right_column_tab {
super::state::RightColumnTab::Activity => super::state::RightColumnTab::Diff,
super::state::RightColumnTab::Diff => super::state::RightColumnTab::Activity,
};
return prepend_non_standalone_clear(
key,
InputCommand::display(DisplayCommand::Focus(FocusCommand::RightColumnTab(tab))),
);
}
if state.can_switch_main_tabs()
&& action_for_key(key, KeyContext::Global) == Some(KeyCommand::ToggleHeaderModalsPinned)
{
return prepend_non_standalone_clear(
key,
InputCommand::display(DisplayCommand::ToggleHeaderModalsPinned),
);
}
if super::diff::owns_key(state, key) {
return InputCommand::display(DisplayCommand::Diff(super::diff::DiffCommand::Key(key)));
}
if standalone_ctrl {
if matches!(
state.top_modal(),
Some(
super::state::ModalKind::SubagentViewer
| super::state::ModalKind::ActivityDetail
| super::state::ModalKind::Summary
| super::state::ModalKind::ActivityTree
| super::state::ModalKind::SessionFiles
)
) {
return InputCommand::None;
}
return InputCommand::display(DisplayCommand::StandaloneCtrl {
kind: key.kind,
now,
});
}
let viewport = viewports.prompt.unwrap_or(DEFAULT_PROMPT_VIEWPORT).safe();
let transcript_viewport = viewports
.transcript
.unwrap_or(DEFAULT_TRANSCRIPT_VIEWPORT)
.safe();
let activity_tree_viewport = viewports
.activity_tree
.unwrap_or(DEFAULT_ACTIVITY_TREE_VIEWPORT)
.safe();
let detail_viewport = viewports.detail.unwrap_or(DEFAULT_DETAIL_VIEWPORT).safe();
let system_prompt_viewport = viewports
.system_prompt
.unwrap_or(DEFAULT_SYSTEM_PROMPT_VIEWPORT)
.safe();
let rewind_modal_viewport = viewports
.rewind_modal
.unwrap_or(DEFAULT_SYSTEM_PROMPT_VIEWPORT)
.safe();
let usage_modal_viewport = viewports
.usage_modal
.unwrap_or(DEFAULT_SYSTEM_PROMPT_VIEWPORT)
.safe();
let connect_provider_viewport = viewports.connect_provider.map(PickerViewport::safe);
let logout_picker_viewport = viewports.logout_picker.map(PickerViewport::safe);
let model_picker_viewport = viewports.model_picker.map(PickerViewport::safe);
let skills_modal_viewport = viewports.skills_modal.map(PickerViewport::safe);
let mcp_modal_viewport = viewports.mcp_modal.map(PickerViewport::safe);
let tools_modal_viewport = viewports.tools_modal.map(PickerViewport::safe);
let subagents_modal_viewport = viewports.subagents_modal.map(PickerViewport::safe);
let sessions_modal_viewport = viewports.sessions_modal.map(SessionPickerViewport::safe);
let theme_picker_viewport = viewports.theme_picker.map(PickerViewport::safe);
let modal_viewports = modal::ModalKeyViewports {
system_prompt: system_prompt_viewport,
summary: viewports.summary.unwrap_or_default(),
rewind_modal: rewind_modal_viewport,
usage_modal: usage_modal_viewport,
connect_provider: connect_provider_viewport,
connect_provider_modal_visible: viewports.connect_provider_modal_visible,
connect_provider_custom_field_visible: viewports.connect_provider_custom_field_visible,
connect_provider_oauth_controls_visible: viewports.connect_provider_oauth_controls_visible,
connect_provider_oauth_fallback_visible: viewports.connect_provider_oauth_fallback_visible,
logout_picker: logout_picker_viewport,
logout_confirmation_action_row: viewports.logout_confirmation_action_row,
model_picker: model_picker_viewport,
sessions_modal: sessions_modal_viewport,
subagent_viewer: viewports
.subagent_viewer
.unwrap_or(DEFAULT_SYSTEM_PROMPT_VIEWPORT)
.safe(),
skills_modal: skills_modal_viewport,
skills_scope_visible: viewports.skills_scope_visible,
mcp_modal: mcp_modal_viewport,
tools_modal: tools_modal_viewport,
tools_scope_visible: viewports.tools_scope_visible,
subagents_modal: subagents_modal_viewport,
subagents_scope_visible: viewports.subagents_scope_visible,
theme_picker: theme_picker_viewport,
theme_query_visible: viewports.theme_query_visible,
};
if state.header_modal_visible() {
let toggle = match (key.code, key.modifiers) {
(KeyCode::Char('3'), KeyModifiers::ALT) => Some(DisplayCommand::ToggleSummaryModal),
(KeyCode::Char('1'), KeyModifiers::ALT) => {
Some(DisplayCommand::ToggleActivityTreeModal)
}
(KeyCode::Char('t'), KeyModifiers::CONTROL) => {
Some(DisplayCommand::Focus(FocusCommand::ActivityTree))
}
(KeyCode::Char('2'), KeyModifiers::ALT) => {
Some(DisplayCommand::ToggleSessionFilesModal)
}
_ => None,
};
if let Some(command) = toggle {
return InputCommand::display(command);
}
if !state.prompt_accepts_input()
&& let Some(ch) =
printable_char_for_text_input(key).filter(|ch| char_autofocuses_prompt(*ch))
{
return prepend_non_standalone_clear(
key,
InputCommand::display(DisplayCommand::batch([
DisplayCommand::Focus(FocusCommand::Prompt { close_help: true }),
DisplayCommand::Prompt(PromptCommand::Edit {
operation: PromptEditCommand::InsertChar(ch),
viewport,
refresh_autocomplete: true,
}),
])),
);
}
if key.code == KeyCode::Tab && key.modifiers.is_empty() && !state.prompt_accepts_input() {
return InputCommand::display(DisplayCommand::Focus(FocusCommand::NextPrimaryPane));
}
if key.code == KeyCode::Esc
&& key.modifiers.is_empty()
&& !(state.prompt_accepts_input() && state.autocomplete_visible())
{
if state.modals.header_modals_pinned {
return InputCommand::None;
}
return InputCommand::display(match state.top_modal() {
Some(super::state::ModalKind::Summary) => DisplayCommand::ToggleSummaryModal,
Some(super::state::ModalKind::ActivityTree) => {
DisplayCommand::ToggleActivityTreeModal
}
_ => DisplayCommand::ToggleSessionFilesModal,
});
}
}
if state.is_activity_tree_focused() {
if key.code == KeyCode::Char('c') && key.modifiers == KeyModifiers::ALT {
if state.running_prompt_cancelable() {
return InputCommand::action(InputAction::CancelRunningPrompt);
}
if state.startup_prompt_queued {
return InputCommand::action(InputAction::CancelStartupPrompt);
}
}
return keybindings::action_for_key(key, KeyContext::ActivityTree)
.filter(|action| {
matches!(
action,
KeyCommand::OpenActivityDetail
| KeyCommand::MoveSelectionUp
| KeyCommand::MoveSelectionDown
| KeyCommand::CollapseSelected
| KeyCommand::ExpandSelected
| KeyCommand::ExpandAll
| KeyCommand::CollapseAll
| KeyCommand::SelectFailed
| KeyCommand::SelectRunning
)
})
.map(|action| {
classify_keybinding_action(
action,
state,
viewport,
transcript_viewport,
activity_tree_viewport,
detail_viewport,
)
})
.unwrap_or(InputCommand::None);
}
if state.top_modal() == Some(super::state::ModalKind::ActivityDetail) {
return match key.code {
KeyCode::Esc => InputCommand::display(DisplayCommand::CloseActivityDetail),
_ => keybindings::activity_detail_action_for_key(key)
.map(|action| {
classify_keybinding_action(
action,
state,
viewport,
transcript_viewport,
activity_tree_viewport,
detail_viewport,
)
})
.unwrap_or(InputCommand::None),
};
}
if state.top_modal() == Some(super::state::ModalKind::SubagentViewer) {
if key.code == KeyCode::Char('c') && key.modifiers == KeyModifiers::ALT {
return InputCommand::action(InputAction::CancelSelectedSubagent);
}
let completion_visible = state
.selected_subagent_prompt()
.is_some_and(|draft| super::autocomplete::visible(&draft.autocomplete));
let navigation = matches!(
key.code,
KeyCode::Esc | KeyCode::BackTab | KeyCode::PageUp | KeyCode::PageDown
) || (key.code == KeyCode::Tab && !completion_visible);
if state.subagent_prompt_editable()
&& !navigation
&& let Some(viewport) = viewports.subagent_prompt
{
return InputCommand::action(InputAction::SubagentPromptKey(key, viewport));
}
return prepend_non_standalone_clear(
key,
modal::classify_modal_key(key, state, modal_viewports).unwrap_or(InputCommand::None),
);
}
let is_alt_c = key.code == KeyCode::Char('c') && key.modifiers == KeyModifiers::ALT;
if is_alt_c {
if state.running_prompt_cancelable() {
return prepend_non_standalone_clear(
key,
InputCommand::action(InputAction::CancelRunningPrompt),
);
}
if state.startup_prompt_queued {
return prepend_non_standalone_clear(
key,
InputCommand::action(InputAction::CancelStartupPrompt),
);
}
}
if let Some(command) = modal::classify_modal_key(key, state, modal_viewports) {
return prepend_non_standalone_clear(key, command);
}
if state.is_prompt_focused() && state.autocomplete_visible() {
let command =
match (key.code, key.modifiers) {
(KeyCode::Tab | KeyCode::Enter, KeyModifiers::NONE) => InputCommand::display(
DisplayCommand::Prompt(PromptCommand::AcceptAutocomplete { viewport }),
),
(KeyCode::Up, KeyModifiers::NONE) => {
InputCommand::display(DisplayCommand::Prompt(PromptCommand::MoveAutocompleteUp))
}
(KeyCode::Down, KeyModifiers::NONE) => InputCommand::display(
DisplayCommand::Prompt(PromptCommand::MoveAutocompleteDown),
),
(KeyCode::Esc, KeyModifiers::NONE) => InputCommand::display(
DisplayCommand::Prompt(PromptCommand::DismissAutocomplete),
),
_ => InputCommand::None,
};
if !matches!(command, InputCommand::None) {
return prepend_non_standalone_clear(key, command);
}
}
if state.visible_prompt_history_suffix().is_some() && key.modifiers == KeyModifiers::NONE {
let command = match key.code {
KeyCode::Right => Some(PromptCommand::AcceptPromptHistory { viewport }),
KeyCode::Esc => Some(PromptCommand::DismissPromptHistory),
_ => None,
};
if let Some(command) = command {
return prepend_non_standalone_clear(
key,
InputCommand::display(DisplayCommand::Prompt(command)),
);
}
}
if key.code == KeyCode::Tab && key.modifiers == KeyModifiers::NONE {
return prepend_non_standalone_clear(
key,
InputCommand::display(DisplayCommand::Focus(FocusCommand::NextPrimaryPane)),
);
}
if key.code == KeyCode::BackTab
&& (key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT)
{
return prepend_non_standalone_clear(
key,
InputCommand::display(DisplayCommand::Focus(FocusCommand::PreviousPrimaryPane)),
);
}
if key.code == KeyCode::Char('c')
&& key.modifiers == KeyModifiers::CONTROL
&& let Some(text) = state.prompt_selection_text()
{
return prepend_non_standalone_clear(
key,
InputCommand::action(InputAction::CopySelectedPrompt(text)),
);
}
let active_context = match state.focus_pane {
TuiFocusPane::Prompt => KeyContext::Prompt,
TuiFocusPane::Transcript => KeyContext::Transcript,
};
if state.is_prompt_focused()
&& key.code == KeyCode::Enter
&& shifted_enter_modifiers(key.modifiers)
{
return prepend_non_standalone_clear(
key,
InputCommand::display(DisplayCommand::Prompt(PromptCommand::Edit {
operation: PromptEditCommand::InsertNewline,
viewport,
refresh_autocomplete: true,
})),
);
}
if let Some(action) = action_for_key(key, active_context) {
let command = classify_keybinding_action(
action,
state,
viewport,
transcript_viewport,
activity_tree_viewport,
detail_viewport,
);
return prepend_non_standalone_clear(key, command);
}
if !state.is_prompt_focused()
&& let Some(ch) =
printable_char_for_text_input(key).filter(|ch| char_autofocuses_prompt(*ch))
{
return prepend_non_standalone_clear(
key,
InputCommand::display(DisplayCommand::batch([
DisplayCommand::Focus(FocusCommand::Prompt { close_help: true }),
DisplayCommand::Prompt(PromptCommand::Edit {
operation: PromptEditCommand::InsertChar(ch),
viewport,
refresh_autocomplete: true,
}),
])),
);
}
if state.is_prompt_focused() {
let command = match key.code {
KeyCode::Backspace if key.modifiers == KeyModifiers::NONE => {
Some(PromptEditCommand::Backspace)
}
KeyCode::Delete if key.modifiers == KeyModifiers::NONE => {
Some(PromptEditCommand::Delete)
}
KeyCode::Char(_) => {
printable_char_for_text_input(key).map(PromptEditCommand::InsertChar)
}
_ => None,
};
if let Some(operation) = command {
return prepend_non_standalone_clear(
key,
InputCommand::display(DisplayCommand::Prompt(PromptCommand::Edit {
operation,
viewport,
refresh_autocomplete: true,
})),
);
}
}
let _ = autocomplete_candidates;
prepend_non_standalone_clear(key, InputCommand::None)
}
pub(super) fn next_primary_agent_id(state: &MissionControlState) -> Option<String> {
let next = match (
state.selected_primary_agent,
state.primary_agents.is_empty(),
) {
(_, true) => None,
(None, false) => Some(0),
(Some(index), false) if index + 1 < state.primary_agents.len() => Some(index + 1),
(Some(_), false) => None,
};
next.and_then(|index| {
state
.primary_agents
.get(index)
.map(|entry| entry.id.clone())
})
}
pub(super) fn is_quit_prompt(prompt: &str) -> bool {
prompt
.trim()
.split_once(char::is_whitespace)
.map(|(command, _)| command)
.unwrap_or_else(|| prompt.trim())
== "/quit"
}
fn prepend_non_standalone_clear(key: KeyEvent, command: InputCommand) -> InputCommand {
let prefix = DisplayCommand::ClearStandaloneCtrl {
suppress_release: key.modifiers.contains(KeyModifiers::CONTROL),
};
let (action, display) = command.into_parts();
let display = match display {
Some(display) => DisplayCommand::batch([prefix, display]),
None => prefix,
};
InputCommand::action_with_display(action, display)
}