pub(in crate::tui) mod autocomplete;
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 selection;
pub(in crate::tui) mod submit_command;
use self::keybindings::{
KeyContext, TuiAction, action_for_key, help_text as keybindings_help_text,
};
use super::state::{AutocompleteCandidate, MissionControlState, TuiFocusPane};
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers, ModifierKeyCode};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy)]
pub(crate) struct PromptViewport {
pub(crate) visible_rows: u16,
pub(crate) wrap_width: u16,
}
impl PromptViewport {
fn safe(self) -> Self {
Self {
visible_rows: self.visible_rows.max(1),
wrap_width: self.wrap_width.max(1),
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct TranscriptViewport {
pub(crate) visible_rows: u16,
pub(crate) wrap_width: u16,
}
impl TranscriptViewport {
fn safe(self) -> Self {
Self {
visible_rows: self.visible_rows.max(1),
wrap_width: self.wrap_width.max(1),
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ActivityTreeViewport {
pub(crate) visible_rows: u16,
}
impl ActivityTreeViewport {
fn safe(self) -> Self {
Self {
visible_rows: self.visible_rows.max(1),
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct DetailViewport {
pub(crate) visible_rows: u16,
pub(crate) wrap_width: u16,
}
impl DetailViewport {
fn safe(self) -> Self {
Self {
visible_rows: self.visible_rows,
wrap_width: self.wrap_width.max(1),
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct SystemPromptViewport {
pub(crate) visible_rows: u16,
pub(crate) wrap_width: u16,
}
impl SystemPromptViewport {
fn safe(self) -> Self {
Self {
visible_rows: self.visible_rows.max(1),
wrap_width: self.wrap_width.max(1),
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct PickerViewport {
pub(crate) visible_rows: u16,
}
impl PickerViewport {
fn safe(self) -> Self {
Self {
visible_rows: self.visible_rows.max(1),
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct SessionPickerViewport {
pub(crate) list_rows: u16,
pub(crate) preview_rows: u16,
pub(crate) preview_width: u16,
pub(crate) preview_visible: bool,
}
impl SessionPickerViewport {
fn safe(self) -> Self {
Self {
list_rows: self.list_rows.max(1),
preview_rows: self.preview_rows.max(1),
preview_width: self.preview_width.max(1),
preview_visible: self.preview_visible,
}
}
}
const DEFAULT_PROMPT_VIEWPORT: PromptViewport = PromptViewport {
visible_rows: 1,
wrap_width: 80,
};
const DEFAULT_TRANSCRIPT_VIEWPORT: TranscriptViewport = TranscriptViewport {
visible_rows: 1,
wrap_width: 80,
};
const DEFAULT_ACTIVITY_TREE_VIEWPORT: ActivityTreeViewport =
ActivityTreeViewport { visible_rows: 1 };
const DEFAULT_DETAIL_VIEWPORT: DetailViewport = DetailViewport {
visible_rows: 1,
wrap_width: 80,
};
const DEFAULT_SYSTEM_PROMPT_VIEWPORT: SystemPromptViewport = SystemPromptViewport {
visible_rows: 10,
wrap_width: 80,
};
pub(crate) const DEFAULT_PICKER_VISIBLE_ROWS: u16 = 10;
const DEFAULT_PICKER_VIEWPORT: PickerViewport = PickerViewport {
visible_rows: DEFAULT_PICKER_VISIBLE_ROWS,
};
const DEFAULT_SESSION_PICKER_VIEWPORT: SessionPickerViewport = SessionPickerViewport {
list_rows: DEFAULT_PICKER_VISIBLE_ROWS,
preview_rows: DEFAULT_PICKER_VISIBLE_ROWS,
preview_width: 80,
preview_visible: true,
};
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum InputAction {
None,
Submit(String),
SelectModel(String),
SelectSession(String),
SetSkillEnabled { name: String, enabled: bool },
ToggleSkillsSettingsScope,
SetMcpServerEnabled { name: String, enabled: bool },
SetToolEnabled { name: String, enabled: bool },
ToggleToolsSettingsScope,
SetSubagentProfileEnabled { id: String, enabled: bool },
ToggleSubagentsSettingsScope,
SetModelEnabled { id: String, enabled: bool },
ToggleModelsSettingsScope,
PrimaryAgentSelectionChanged(Option<String>),
ThinkingLevelSelectionChanged(crate::thinking::ThinkingLevel),
SelectLoginProvider(String),
SelectLogoutProvider(String),
SubmitLoginFallback(String),
SubmitCustomProviderSetupStep,
CopyLoginUrl,
OpenLoginUrl,
CancelLogin,
ConfirmLogout(String),
CancelLogout,
ConfirmCustomProviderReplacement,
CancelCustomProviderReplacement,
ConfirmRewind,
CancelRewind,
CancelRunningPrompt,
OpenSetModel,
Exit,
}
pub(crate) fn help_text() -> &'static str {
keybindings_help_text()
}
#[cfg(test)]
use self::keybindings::default_keybindings;
#[cfg(test)]
pub(crate) fn handle_key(key: KeyEvent, state: &mut MissionControlState) -> InputAction {
handle_key_with_viewport(key, state, None, &[])
}
#[cfg(test)]
pub(crate) fn handle_key_with_viewport(
key: KeyEvent,
state: &mut MissionControlState,
prompt_viewport: Option<PromptViewport>,
autocomplete_candidates: &[AutocompleteCandidate],
) -> InputAction {
handle_key_with_viewports(key, state, prompt_viewport, None, autocomplete_candidates)
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct KeyViewports {
pub(crate) prompt: Option<PromptViewport>,
pub(crate) transcript: Option<TranscriptViewport>,
pub(crate) activity_tree: Option<ActivityTreeViewport>,
pub(crate) detail: Option<DetailViewport>,
pub(crate) system_prompt: Option<SystemPromptViewport>,
pub(crate) rewind_modal: Option<SystemPromptViewport>,
pub(crate) usage_modal: Option<SystemPromptViewport>,
pub(crate) login_picker: Option<PickerViewport>,
pub(crate) model_picker: Option<PickerViewport>,
pub(crate) sessions_modal: Option<SessionPickerViewport>,
pub(crate) models_modal: Option<PickerViewport>,
}
pub(crate) fn handle_key_with_all_viewports(
key: KeyEvent,
state: &mut MissionControlState,
viewports: KeyViewports,
autocomplete_candidates: &[AutocompleteCandidate],
) -> InputAction {
handle_key_inner_at(
key,
state,
viewports,
autocomplete_candidates,
Instant::now(),
)
}
#[cfg(test)]
pub(crate) fn handle_key_at(
key: KeyEvent,
state: &mut MissionControlState,
viewports: KeyViewports,
autocomplete_candidates: &[AutocompleteCandidate],
now: Instant,
) -> InputAction {
handle_key_inner_at(key, state, viewports, autocomplete_candidates, now)
}
#[cfg(test)]
pub(crate) fn handle_key_with_viewports(
key: KeyEvent,
state: &mut MissionControlState,
prompt_viewport: Option<PromptViewport>,
transcript_viewport: Option<TranscriptViewport>,
autocomplete_candidates: &[AutocompleteCandidate],
) -> InputAction {
handle_key_inner_at(
key,
state,
KeyViewports {
prompt: prompt_viewport,
transcript: transcript_viewport,
..KeyViewports::default()
},
autocomplete_candidates,
Instant::now(),
)
}
#[cfg(test)]
pub(crate) fn handle_paste_with_viewport(
text: &str,
state: &mut MissionControlState,
prompt_viewport: Option<PromptViewport>,
autocomplete_candidates: &[AutocompleteCandidate],
) -> InputAction {
let viewport = prompt_viewport.unwrap_or(DEFAULT_PROMPT_VIEWPORT).safe();
handle_paste_inner(text, state, viewport, autocomplete_candidates)
}
pub(crate) fn handle_paste_with_all_viewports(
text: &str,
state: &mut MissionControlState,
viewports: KeyViewports,
autocomplete_candidates: &[AutocompleteCandidate],
) -> InputAction {
let viewport = viewports.prompt.unwrap_or(DEFAULT_PROMPT_VIEWPORT).safe();
handle_paste_inner(text, state, viewport, autocomplete_candidates)
}
fn handle_paste_inner(
text: &str,
state: &mut MissionControlState,
viewport: PromptViewport,
autocomplete_candidates: &[AutocompleteCandidate],
) -> InputAction {
if prompt_accepts_paste(state) && !text.is_empty() {
state.insert_prompt_str(text, viewport.visible_rows, viewport.wrap_width);
refresh_autocomplete_after_key(state, autocomplete_candidates);
}
InputAction::None
}
pub(crate) fn prompt_accepts_paste(state: &MissionControlState) -> bool {
state.is_prompt_focused()
&& !state.session_picker_visible()
&& !state.skills_modal_visible()
&& !state.mcp_modal_visible()
&& !state.tools_modal_visible()
&& !state.subagents_modal_visible()
&& !state.models_modal_visible()
&& !state.system_prompt_modal_visible()
&& !state.usage_modal_visible()
&& !state.rewind_modal_visible()
&& state.modals.logout_confirmation.is_none()
&& state
.modals
.custom_provider_replacement_confirmation
.is_none()
&& state.modals.custom_provider_setup.is_none()
&& state.modals.active_login.is_none()
&& state.modals.login_picker.is_none()
&& state.modals.model_picker.is_none()
}
fn handle_key_inner_at(
key: KeyEvent,
state: &mut MissionControlState,
viewports: KeyViewports,
autocomplete_candidates: &[AutocompleteCandidate],
now: Instant,
) -> InputAction {
let standalone_ctrl = is_standalone_ctrl_key(key);
if key.kind == KeyEventKind::Release && !standalone_ctrl {
return InputAction::None;
}
if standalone_ctrl {
handle_standalone_ctrl_key(key, state, now);
return InputAction::None;
}
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 login_picker_viewport = viewports
.login_picker
.unwrap_or(DEFAULT_PICKER_VIEWPORT)
.safe();
let model_picker_viewport = viewports
.model_picker
.unwrap_or(DEFAULT_PICKER_VIEWPORT)
.safe();
let sessions_modal_viewport = viewports
.sessions_modal
.unwrap_or(DEFAULT_SESSION_PICKER_VIEWPORT)
.safe();
let models_modal_viewport = viewports
.models_modal
.unwrap_or(DEFAULT_PICKER_VIEWPORT)
.safe();
let modal_viewports = modal::ModalKeyViewports {
system_prompt: system_prompt_viewport,
rewind_modal: rewind_modal_viewport,
usage_modal: usage_modal_viewport,
login_picker: login_picker_viewport,
model_picker: model_picker_viewport,
sessions_modal: sessions_modal_viewport,
models_modal: models_modal_viewport,
};
if let Some(action) = modal::handle_modal_key(key, state, modal_viewports) {
clear_standalone_ctrl_after_non_standalone_key(key, state);
return action;
}
clear_standalone_ctrl_after_non_standalone_key(key, state);
if state.running_prompt_cancelable()
&& key.code == KeyCode::Char('c')
&& key.modifiers == KeyModifiers::ALT
{
return InputAction::CancelRunningPrompt;
}
if state.is_prompt_focused() && state.autocomplete_visible() {
match (key.code, key.modifiers) {
(KeyCode::Tab | KeyCode::Enter, KeyModifiers::NONE) => {
state.accept_autocomplete(viewport.visible_rows, viewport.wrap_width);
return InputAction::None;
}
(KeyCode::Up, KeyModifiers::NONE) => {
state.move_autocomplete_selection_up();
return InputAction::None;
}
(KeyCode::Down, KeyModifiers::NONE) => {
state.move_autocomplete_selection_down();
return InputAction::None;
}
(KeyCode::Esc, KeyModifiers::NONE) => {
state.dismiss_autocomplete_for_current_token();
return InputAction::None;
}
_ => {}
}
}
let active_context = match state.focus_pane {
TuiFocusPane::Prompt => KeyContext::Prompt,
TuiFocusPane::Transcript => KeyContext::Transcript,
TuiFocusPane::ActivityTree => KeyContext::ActivityTree,
TuiFocusPane::ActivityDetail => KeyContext::ActivityDetail,
};
if state.is_prompt_focused()
&& key.code == KeyCode::Enter
&& shifted_enter_modifiers(key.modifiers)
{
let result = apply_action(
TuiAction::InsertPromptNewline,
state,
viewport,
transcript_viewport,
activity_tree_viewport,
detail_viewport,
);
refresh_autocomplete_after_key(state, autocomplete_candidates);
return result;
}
if let Some(action) = action_for_key(key, active_context) {
let result = apply_action(
action,
state,
viewport,
transcript_viewport,
activity_tree_viewport,
detail_viewport,
);
if !matches!(
action,
TuiAction::ScrollTranscriptUp
| TuiAction::ScrollTranscriptDown
| TuiAction::FocusTranscriptLayout
| TuiAction::FocusActivityLayout
) {
refresh_autocomplete_after_key(state, autocomplete_candidates);
}
return result;
}
if state.is_prompt_focused() {
match key.code {
KeyCode::Backspace if key.modifiers == KeyModifiers::NONE => {
state.backspace_prompt_char(viewport.visible_rows, viewport.wrap_width);
refresh_autocomplete_after_key(state, autocomplete_candidates);
}
KeyCode::Delete if key.modifiers == KeyModifiers::NONE => {
state.delete_prompt_char(viewport.visible_rows, viewport.wrap_width);
refresh_autocomplete_after_key(state, autocomplete_candidates);
}
KeyCode::Char(_) => {
if let Some(ch) = printable_char_for_text_input(key) {
state.insert_prompt_char(ch, viewport.visible_rows, viewport.wrap_width);
refresh_autocomplete_after_key(state, autocomplete_candidates);
}
}
_ => {}
}
}
InputAction::None
}
const DOUBLE_CTRL_WINDOW: Duration = Duration::from_millis(500);
fn handle_standalone_ctrl_key(key: KeyEvent, state: &mut MissionControlState, now: Instant) {
match key.kind {
KeyEventKind::Press => {
handle_standalone_ctrl_tap(state, now);
state.suppress_next_standalone_ctrl_release();
}
KeyEventKind::Release => {
if !state.take_suppressed_standalone_ctrl_release() {
handle_standalone_ctrl_tap(state, now);
}
}
KeyEventKind::Repeat => {}
}
}
fn handle_standalone_ctrl_tap(state: &mut MissionControlState, now: Instant) {
if state
.last_standalone_ctrl
.is_some_and(|last| now.duration_since(last) <= DOUBLE_CTRL_WINDOW)
{
state.focus_prompt();
state.show_help = false;
state.clear_pending_standalone_ctrl();
} else {
state.arm_standalone_ctrl(now);
}
}
fn clear_standalone_ctrl_after_non_standalone_key(key: KeyEvent, state: &mut MissionControlState) {
let suppress_ctrl_release = key.modifiers.contains(KeyModifiers::CONTROL);
state.clear_pending_standalone_ctrl();
if suppress_ctrl_release {
state.suppress_next_standalone_ctrl_release();
}
}
fn refresh_autocomplete_after_key(
state: &mut MissionControlState,
candidates: &[AutocompleteCandidate],
) {
if state.is_prompt_focused() {
state.recompute_autocomplete(candidates);
} else {
state.hide_autocomplete();
}
}
fn is_standalone_ctrl_key(key: KeyEvent) -> bool {
match key.code {
KeyCode::Modifier(ModifierKeyCode::LeftControl | ModifierKeyCode::RightControl) => {
!key.modifiers.intersects(
KeyModifiers::SHIFT
| KeyModifiers::ALT
| KeyModifiers::SUPER
| KeyModifiers::HYPER
| KeyModifiers::META,
)
}
_ => false,
}
}
fn shifted_enter_modifiers(modifiers: KeyModifiers) -> bool {
modifiers.contains(KeyModifiers::SHIFT)
}
pub(super) fn printable_char_for_text_input(key: KeyEvent) -> Option<char> {
let KeyCode::Char(ch) = key.code else {
return None;
};
if key.modifiers.is_empty() {
Some(ch)
} else if key.modifiers == KeyModifiers::SHIFT {
Some(shift_printable_char(ch))
} else {
None
}
}
pub(crate) fn text_char_for_key_burst(key: KeyEvent) -> Option<char> {
if key.kind == KeyEventKind::Release {
return None;
}
if key.modifiers == KeyModifiers::NONE {
return match key.code {
KeyCode::Enter => Some('\n'),
KeyCode::Tab => Some('\t'),
_ => printable_char_for_text_input(key),
};
}
printable_char_for_text_input(key)
}
pub(crate) fn text_for_key_burst(keys: &[KeyEvent]) -> Option<String> {
keys.iter().copied().map(text_char_for_key_burst).collect()
}
fn shift_printable_char(ch: char) -> char {
if ch.is_ascii_lowercase() {
return ch.to_ascii_uppercase();
}
match ch {
'`' => '~',
'1' => '!',
'2' => '@',
'3' => '#',
'4' => '$',
'5' => '%',
'6' => '^',
'7' => '&',
'8' => '*',
'9' => '(',
'0' => ')',
'-' => '_',
'=' => '+',
'[' => '{',
']' => '}',
'\\' => '|',
';' => ':',
'\'' => '"',
',' => '<',
'.' => '>',
'/' => '?',
_ => ch,
}
}
fn apply_action(
action: TuiAction,
state: &mut MissionControlState,
prompt_viewport: PromptViewport,
transcript_viewport: TranscriptViewport,
activity_tree_viewport: ActivityTreeViewport,
detail_viewport: DetailViewport,
) -> InputAction {
let has_activity = state.visible_node_count() != 0;
match action {
TuiAction::SubmitPrompt => {
let prompt = state.take_prompt_for_submit();
if prompt
.trim()
.split_once(char::is_whitespace)
.map(|(command, _)| command)
.unwrap_or_else(|| prompt.trim())
== "/quit"
{
return InputAction::Exit;
}
return InputAction::Submit(prompt);
}
TuiAction::InsertPromptNewline => {
state.insert_prompt_char(
'\n',
prompt_viewport.visible_rows,
prompt_viewport.wrap_width,
);
}
TuiAction::Exit => return InputAction::Exit,
TuiAction::ClearInputOrExit => {
if state.input.is_empty() {
return InputAction::Exit;
}
state.clear_prompt_input();
}
TuiAction::FocusPrompt => {
state.focus_prompt();
state.show_help = false;
}
TuiAction::FocusActivityTree => state.focus_activity_tree(),
TuiAction::FocusActivityDetail => state.focus_activity_detail(),
TuiAction::CyclePrimaryAgent => {
let before = state.selected_primary_agent_id().map(ToString::to_string);
state.cycle_primary_agent();
let after = state.selected_primary_agent_id().map(ToString::to_string);
if after != before {
return InputAction::PrimaryAgentSelectionChanged(after);
}
}
TuiAction::CycleThinkingLevel => {
if let Some(next) = state.next_thinking_level() {
state.refresh_thinking_levels(next, state.thinking_levels.clone());
return InputAction::ThinkingLevelSelectionChanged(next);
}
state.status = "thinking level is not configurable for this model".to_string();
}
TuiAction::ToggleHelp => {
state.show_help = !state.show_help;
if state.show_help {
state.set_scroll_offset(&state.scroll_views.help, 0);
}
}
TuiAction::OpenSetModel => return InputAction::OpenSetModel,
TuiAction::FocusTranscriptLayout => state.focus_transcript_layout(),
TuiAction::FocusActivityLayout => state.focus_activity_layout(),
TuiAction::MoveSelectionUp if has_activity => {
let previous = state.selected;
state.selected = state.selected.saturating_sub(1);
state.activity_follow_tail = false;
if state.selected != previous {
state.reset_detail();
}
state.ensure_activity_selection_visible(activity_tree_viewport.visible_rows);
}
TuiAction::MoveSelectionDown if has_activity => {
let previous = state.selected;
let max = state.visible_node_count().saturating_sub(1);
state.selected = (state.selected + 1).min(max);
state.activity_follow_tail = state.selected >= max;
if state.selected != previous {
state.reset_detail();
}
state.ensure_activity_selection_visible(activity_tree_viewport.visible_rows);
}
TuiAction::CollapseSelected if has_activity => {
let previous_selection = state.selected_activity_id();
if let Some(id) = state.visible_node_id_at(state.selected)
&& state.has_activity_children(&id)
&& state.expanded.remove(&id)
{
state.invalidate_activity_render_cache();
}
state.normalize_selection_after_visible_change(previous_selection);
state.ensure_activity_selection_visible(activity_tree_viewport.visible_rows);
}
TuiAction::ExpandSelected if has_activity => {
let previous_selection = state.selected_activity_id();
if let Some(id) = state.visible_node_id_at(state.selected)
&& state.has_activity_children(&id)
&& state.expanded.insert(id)
{
state.invalidate_activity_render_cache();
}
state.normalize_selection_after_visible_change(previous_selection);
state.ensure_activity_selection_visible(activity_tree_viewport.visible_rows);
}
TuiAction::ExpandAll if has_activity => {
let previous_selection = state.selected_activity_id();
for id in state.visible_activity_ids() {
state.expanded.insert(id);
}
state.invalidate_activity_render_cache();
state.normalize_selection_after_visible_change(previous_selection);
state.ensure_activity_selection_visible(activity_tree_viewport.visible_rows);
}
TuiAction::CollapseAll if has_activity => {
let previous_selection = state.selected_activity_id();
state.expanded.clear();
state.invalidate_activity_render_cache();
state.normalize_selection_after_visible_change(previous_selection);
state.ensure_activity_selection_visible(activity_tree_viewport.visible_rows);
}
TuiAction::SelectFailed if has_activity => {
select_status(state, crate::output::ActivityStatus::Failed);
state.ensure_activity_selection_visible(activity_tree_viewport.visible_rows);
}
TuiAction::SelectRunning if has_activity => {
select_status(state, crate::output::ActivityStatus::Running);
state.ensure_activity_selection_visible(activity_tree_viewport.visible_rows);
}
TuiAction::ScrollDetailUp => {
state.scroll_detail_up_by(1, detail_viewport.visible_rows, detail_viewport.wrap_width);
}
TuiAction::ScrollDetailDown => {
state.scroll_detail_down_by(
1,
detail_viewport.visible_rows,
detail_viewport.wrap_width,
);
}
TuiAction::ScrollTranscriptUp => {
state.scroll_transcript_up_by(
1,
transcript_viewport.visible_rows,
transcript_viewport.wrap_width,
);
}
TuiAction::ScrollTranscriptDown => {
state.scroll_transcript_down_by(
1,
transcript_viewport.visible_rows,
transcript_viewport.wrap_width,
);
}
TuiAction::MovePromptCursorLeft => {
state.move_prompt_cursor_left(prompt_viewport.visible_rows, prompt_viewport.wrap_width)
}
TuiAction::MovePromptCursorRight => {
state.move_prompt_cursor_right(prompt_viewport.visible_rows, prompt_viewport.wrap_width)
}
TuiAction::MovePromptCursorUp => {
state.move_prompt_cursor_up(prompt_viewport.visible_rows, prompt_viewport.wrap_width)
}
TuiAction::MovePromptCursorDown => {
state.move_prompt_cursor_down(prompt_viewport.visible_rows, prompt_viewport.wrap_width)
}
_ => {}
}
InputAction::None
}
fn select_status(state: &mut MissionControlState, status: crate::output::ActivityStatus) {
let visible_ids = state.visible_activity_ids();
for (index, id) in visible_ids.iter().enumerate() {
let selected = state.nodes.get(id).is_some_and(|node| {
node.status == status
|| (status == crate::output::ActivityStatus::Running
&& node.status == crate::output::ActivityStatus::Writing)
});
if selected {
if state.selected != index {
state.selected = index;
state.activity_follow_tail = index >= visible_ids.len().saturating_sub(1);
state.reset_detail();
}
break;
}
}
}
#[cfg(test)]
#[path = "tests.rs"]
mod tests;
#[cfg(test)]
#[path = "primary_agent_input_tests.rs"]
mod primary_agent_input_tests;