use crate::tui::{input, render, state};
use crossterm::event::KeyEventKind;
use std::time::Instant;
#[derive(Debug, Default)]
pub(super) struct DisplayApplyResult {
pub(super) changed: bool,
pub(super) copy: Option<String>,
}
impl DisplayApplyResult {
fn changed(changed: bool) -> Self {
Self {
changed,
copy: None,
}
}
fn merge(&mut self, other: Self) {
self.changed |= other.changed;
if self.copy.is_none() {
self.copy = other.copy;
}
}
}
pub(super) fn apply_display_command(
command: input::DisplayCommand,
ui_state: &mut state::MissionControlState,
autocomplete_candidates: &[state::AutocompleteCandidate],
) -> DisplayApplyResult {
match command {
input::DisplayCommand::Diff(command) => {
crate::tui::diff::apply(ui_state, command);
DisplayApplyResult::changed(true)
}
input::DisplayCommand::Batch(commands) => {
let mut result = DisplayApplyResult::default();
for command in commands {
result.merge(apply_display_command(
command,
ui_state,
autocomplete_candidates,
));
}
result
}
input::DisplayCommand::ClearStandaloneCtrl { suppress_release } => {
let before = (
ui_state.last_standalone_ctrl,
ui_state.ignore_next_standalone_ctrl_release,
);
ui_state.clear_pending_standalone_ctrl();
if suppress_release {
ui_state.suppress_next_standalone_ctrl_release();
}
DisplayApplyResult::changed(
before
!= (
ui_state.last_standalone_ctrl,
ui_state.ignore_next_standalone_ctrl_release,
),
)
}
input::DisplayCommand::StandaloneCtrl { kind, now } => {
apply_standalone_ctrl(kind, now, ui_state)
}
input::DisplayCommand::OpenActivityDetail => {
DisplayApplyResult::changed(ui_state.open_activity_detail())
}
input::DisplayCommand::CloseActivityDetail => {
ui_state.modals.activity_detail = None;
DisplayApplyResult::changed(true)
}
input::DisplayCommand::ToggleSummaryModal => {
ui_state.toggle_summary_modal();
DisplayApplyResult::changed(true)
}
input::DisplayCommand::ToggleActivityTreeModal => {
ui_state.toggle_activity_tree_modal();
DisplayApplyResult::changed(true)
}
input::DisplayCommand::ToggleSessionFilesModal => {
ui_state.toggle_session_files_modal();
DisplayApplyResult::changed(true)
}
input::DisplayCommand::ToggleHeaderModalsPinned => {
ui_state.toggle_header_modals_pinned();
DisplayApplyResult::changed(true)
}
input::DisplayCommand::ToggleSummary => {
ui_state.focus_summary();
ui_state.summary.requested_enabled = Some(
!ui_state
.summary
.requested_enabled
.unwrap_or(ui_state.summary.enabled),
);
DisplayApplyResult::changed(true)
}
input::DisplayCommand::ScrollSummary {
files,
down,
rows,
width,
height,
} => {
let max = if files {
ui_state.session_files.max_scroll(height)
} else {
ui_state.summary.max_scroll(width.saturating_sub(1), height)
};
let scroll = if files {
&mut ui_state.session_files.scroll
} else {
&mut ui_state.summary.scroll
};
let before = *scroll;
*scroll = if down {
before.min(max).saturating_add(rows).min(max)
} else {
before.min(max).saturating_sub(rows)
};
DisplayApplyResult::changed(before != *scroll)
}
input::DisplayCommand::Focus(command) => apply_focus_command(command, ui_state),
input::DisplayCommand::Prompt(command) => {
apply_prompt_command(command, ui_state, autocomplete_candidates)
}
input::DisplayCommand::Activity(command) => apply_activity_command(command, ui_state),
input::DisplayCommand::Scroll(command) => apply_scroll_command(command, ui_state),
input::DisplayCommand::Modal(command) => apply_modal_command(command, ui_state),
input::DisplayCommand::Mouse(command) => apply_mouse_command(command, ui_state),
input::DisplayCommand::SetPrimaryAgent(id) => {
let changed = ui_state.set_selected_primary_agent_id(id.as_deref());
DisplayApplyResult::changed(changed)
}
input::DisplayCommand::SetThinkingLevel(level) => {
let available = ui_state.thinking_levels.clone();
let changed = ui_state.thinking_level != level;
ui_state.refresh_thinking_levels(level, available);
DisplayApplyResult::changed(changed)
}
input::DisplayCommand::RefreshAutocomplete => {
refresh_autocomplete(ui_state, autocomplete_candidates);
DisplayApplyResult::changed(true)
}
input::DisplayCommand::SetStatus(status) => {
let changed = ui_state.status != status;
ui_state.status = status;
DisplayApplyResult::changed(changed)
}
}
}
fn apply_standalone_ctrl(
kind: KeyEventKind,
now: Instant,
ui_state: &mut state::MissionControlState,
) -> DisplayApplyResult {
match kind {
KeyEventKind::Press => {
if ui_state
.last_standalone_ctrl
.is_some_and(|last| now.duration_since(last) <= DOUBLE_CTRL_WINDOW)
{
ui_state.focus_prompt();
ui_state.show_help = false;
ui_state.clear_pending_standalone_ctrl();
} else {
ui_state.arm_standalone_ctrl(now);
}
ui_state.suppress_next_standalone_ctrl_release();
DisplayApplyResult::changed(true)
}
KeyEventKind::Release => {
if !ui_state.take_suppressed_standalone_ctrl_release() {
if ui_state
.last_standalone_ctrl
.is_some_and(|last| now.duration_since(last) <= DOUBLE_CTRL_WINDOW)
{
ui_state.focus_prompt();
ui_state.show_help = false;
ui_state.clear_pending_standalone_ctrl();
} else {
ui_state.arm_standalone_ctrl(now);
}
}
DisplayApplyResult::changed(true)
}
KeyEventKind::Repeat => DisplayApplyResult::changed(false),
}
}
const DOUBLE_CTRL_WINDOW: std::time::Duration = std::time::Duration::from_millis(500);
fn apply_focus_command(
command: input::FocusCommand,
ui_state: &mut state::MissionControlState,
) -> DisplayApplyResult {
match command {
input::FocusCommand::Prompt { close_help } => {
ui_state.focus_prompt();
if close_help {
ui_state.show_help = false;
}
DisplayApplyResult::changed(true)
}
input::FocusCommand::HeaderModal(modal) => {
ui_state.focus_header_modal(modal);
DisplayApplyResult::changed(true)
}
input::FocusCommand::ActivityTree => {
ui_state.focus_activity_tree();
DisplayApplyResult::changed(true)
}
input::FocusCommand::RightColumnTab(tab) => {
DisplayApplyResult::changed(ui_state.select_right_column_tab(tab))
}
input::FocusCommand::NextPrimaryPane => {
ui_state.focus_next_primary_pane();
DisplayApplyResult::changed(true)
}
input::FocusCommand::PreviousPrimaryPane => {
ui_state.focus_previous_primary_pane();
DisplayApplyResult::changed(true)
}
input::FocusCommand::ToggleHelp => {
ui_state.show_help = !ui_state.show_help;
if ui_state.show_help {
ui_state.set_scroll_offset(&ui_state.scroll_views.help, 0);
}
DisplayApplyResult::changed(true)
}
}
}
fn apply_prompt_command(
command: input::PromptCommand,
ui_state: &mut state::MissionControlState,
autocomplete_candidates: &[state::AutocompleteCandidate],
) -> DisplayApplyResult {
let before_revision = ui_state.prompt_content_revision();
let before_status = ui_state.status.clone();
let mut refresh = false;
match command {
input::PromptCommand::AcceptPromptHistory { viewport } => {
if let Some(suffix) = ui_state.visible_prompt_history_suffix().map(str::to_owned) {
ui_state.insert_prompt_str(&suffix, viewport.visible_rows, viewport.wrap_width);
refresh = true;
}
}
input::PromptCommand::DismissPromptHistory => {
ui_state.dismissed_prompt_history_revision = Some(ui_state.prompt_content_revision());
return DisplayApplyResult::changed(true);
}
input::PromptCommand::Edit {
operation,
viewport,
refresh_autocomplete,
} => {
use input::PromptEditCommand;
match operation {
PromptEditCommand::InsertChar(ch) => {
ui_state.insert_prompt_char(ch, viewport.visible_rows, viewport.wrap_width);
}
PromptEditCommand::InsertNewline => {
ui_state.insert_prompt_char('\n', viewport.visible_rows, viewport.wrap_width);
}
PromptEditCommand::Backspace => {
ui_state.backspace_prompt_char(viewport.visible_rows, viewport.wrap_width);
}
PromptEditCommand::Delete => {
ui_state.delete_prompt_char(viewport.visible_rows, viewport.wrap_width);
}
PromptEditCommand::MoveCursorLeft => {
ui_state.move_prompt_cursor_left(viewport.visible_rows, viewport.wrap_width);
}
PromptEditCommand::MoveCursorRight => {
ui_state.move_prompt_cursor_right(viewport.visible_rows, viewport.wrap_width);
}
PromptEditCommand::MoveCursorUp => {
ui_state.move_prompt_cursor_up(viewport.visible_rows, viewport.wrap_width);
}
PromptEditCommand::MoveCursorDown => {
ui_state.move_prompt_cursor_down(viewport.visible_rows, viewport.wrap_width);
}
PromptEditCommand::MoveWordLeft => {
ui_state.move_prompt_word_left(viewport.visible_rows, viewport.wrap_width);
}
PromptEditCommand::MoveWordRight => {
ui_state.move_prompt_word_right(viewport.visible_rows, viewport.wrap_width);
}
PromptEditCommand::Undo => {
ui_state.undo_prompt_edit();
}
PromptEditCommand::Redo => {
ui_state.redo_prompt_edit();
}
PromptEditCommand::DeleteWord => {
ui_state.delete_prompt_word();
}
PromptEditCommand::DeleteNextWord => {
ui_state.delete_prompt_next_word();
}
PromptEditCommand::ExtendSelectionLeft => {
ui_state
.extend_prompt_selection_left(viewport.visible_rows, viewport.wrap_width);
}
PromptEditCommand::ExtendSelectionRight => {
ui_state
.extend_prompt_selection_right(viewport.visible_rows, viewport.wrap_width);
}
PromptEditCommand::ExtendSelectionUp => {
ui_state.extend_prompt_selection_up(viewport.visible_rows, viewport.wrap_width);
}
PromptEditCommand::ExtendSelectionDown => {
ui_state
.extend_prompt_selection_down(viewport.visible_rows, viewport.wrap_width);
}
PromptEditCommand::SelectAll => ui_state.select_all_prompt(),
PromptEditCommand::Clear => ui_state.clear_prompt_input(),
}
refresh = refresh_autocomplete;
}
input::PromptCommand::InsertText {
text,
viewport,
refresh_autocomplete,
} => {
ui_state.insert_prompt_str(&text, viewport.visible_rows, viewport.wrap_width);
refresh = refresh_autocomplete;
}
input::PromptCommand::TakeForSubmit => {
let _ = ui_state.take_prompt_for_submit();
refresh = true;
}
input::PromptCommand::AcceptAutocomplete { viewport } => {
ui_state.accept_autocomplete(viewport.visible_rows, viewport.wrap_width);
}
input::PromptCommand::MoveAutocompleteUp => {
ui_state.move_autocomplete_selection_up();
}
input::PromptCommand::MoveAutocompleteDown => {
ui_state.move_autocomplete_selection_down();
}
input::PromptCommand::DismissAutocomplete => {
ui_state.dismiss_autocomplete_for_current_token();
}
}
if refresh {
refresh_autocomplete(ui_state, autocomplete_candidates);
}
DisplayApplyResult::changed(
ui_state.prompt_content_revision() != before_revision
|| ui_state.status != before_status
|| refresh,
)
}
fn refresh_autocomplete(
ui_state: &mut state::MissionControlState,
autocomplete_candidates: &[state::AutocompleteCandidate],
) {
if ui_state.is_prompt_focused() {
ui_state.recompute_autocomplete(autocomplete_candidates);
} else {
ui_state.hide_autocomplete();
}
}
fn apply_activity_command(
command: input::ActivityCommand,
ui_state: &mut state::MissionControlState,
) -> DisplayApplyResult {
let before_selected = ui_state.selected;
let before_scroll = ui_state.activity_scroll_offset();
let before_expanded = ui_state.expanded.len();
let before_follow_tail = ui_state.activity_follow_tail;
let has_activity = ui_state.visible_node_count() != 0;
match command {
input::ActivityCommand::MoveUp { visible_rows } if has_activity => {
ui_state.selected = ui_state.selected.saturating_sub(1);
ui_state.activity_follow_tail = false;
if ui_state.selected != before_selected {
ui_state.reset_detail();
}
ui_state.ensure_activity_selection_visible(visible_rows);
}
input::ActivityCommand::MoveDown { visible_rows } if has_activity => {
let max = ui_state.visible_node_count().saturating_sub(1);
ui_state.selected = ui_state.selected.saturating_add(1).min(max);
ui_state.activity_follow_tail = ui_state.selected >= max;
if ui_state.selected != before_selected {
ui_state.reset_detail();
}
ui_state.ensure_activity_selection_visible(visible_rows);
}
input::ActivityCommand::CollapseSelected { visible_rows } if has_activity => {
let previous_selection = ui_state.selected_activity_id();
if let Some(id) = ui_state.visible_node_id_at(ui_state.selected)
&& ui_state.has_activity_children(&id)
&& ui_state.expanded.remove(&id)
{
ui_state.invalidate_activity_render_cache();
}
ui_state.normalize_selection_after_visible_change(previous_selection);
ui_state.ensure_activity_selection_visible(visible_rows);
}
input::ActivityCommand::ExpandSelected { visible_rows } if has_activity => {
let previous_selection = ui_state.selected_activity_id();
if let Some(id) = ui_state.visible_node_id_at(ui_state.selected)
&& ui_state.has_activity_children(&id)
&& ui_state.expanded.insert(id)
{
ui_state.invalidate_activity_render_cache();
}
ui_state.normalize_selection_after_visible_change(previous_selection);
ui_state.ensure_activity_selection_visible(visible_rows);
}
input::ActivityCommand::ExpandAll { visible_rows } if has_activity => {
let previous_selection = ui_state.selected_activity_id();
let expandable_ids = ui_state
.children
.iter()
.filter_map(|(id, children)| (!children.is_empty()).then_some(id.clone()))
.collect::<Vec<_>>();
for id in expandable_ids {
ui_state.expanded.insert(id);
}
ui_state.invalidate_activity_render_cache();
if let Some(previous_selection) = previous_selection.as_ref()
&& let Some(selected) = ui_state
.visible_activity_ids()
.iter()
.position(|id| id == previous_selection)
{
ui_state.selected = selected;
}
ui_state.normalize_selection_after_visible_change(previous_selection);
ui_state.ensure_activity_selection_visible(visible_rows);
}
input::ActivityCommand::CollapseAll { visible_rows } if has_activity => {
let previous_selection = ui_state.selected_activity_id();
ui_state.expanded.clear();
ui_state.invalidate_activity_render_cache();
if let Some(previous_selection) = previous_selection.as_ref()
&& let Some(selected) = ui_state
.visible_activity_ids()
.iter()
.position(|id| id == previous_selection)
{
ui_state.selected = selected;
}
ui_state.normalize_selection_after_visible_change(previous_selection);
ui_state.ensure_activity_selection_visible(visible_rows);
}
input::ActivityCommand::SelectStatus {
status,
visible_rows,
} if has_activity => {
select_status(ui_state, status);
ui_state.ensure_activity_selection_visible(visible_rows);
}
_ => {}
}
DisplayApplyResult::changed(
ui_state.selected != before_selected
|| ui_state.activity_scroll_offset() != before_scroll
|| ui_state.expanded.len() != before_expanded
|| ui_state.activity_follow_tail != before_follow_tail,
)
}
fn select_status(state: &mut state::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;
}
}
}
fn apply_scroll_command(
command: input::ScrollCommand,
ui_state: &mut state::MissionControlState,
) -> DisplayApplyResult {
let changed = match command.target {
input::ScrollTarget::ReleaseNotes => {
crate::tui::release_notes::scroll_rows(ui_state, command.down, command.rows)
}
input::ScrollTarget::Transcript => {
if command.down {
ui_state.scroll_transcript_down_by(
command.rows,
command.visible_rows,
command.wrap_width,
)
} else {
ui_state.scroll_transcript_up_by(
command.rows,
command.visible_rows,
command.wrap_width,
)
}
}
input::ScrollTarget::ActivityTree => {
if command.down {
ui_state.scroll_activity_tree_down_by(command.rows, command.visible_rows)
} else {
ui_state.scroll_activity_tree_up_by(command.rows, command.visible_rows)
}
}
input::ScrollTarget::Detail => {
if command.down {
ui_state.scroll_detail_down_by(
command.rows,
command.visible_rows,
command.wrap_width,
)
} else {
ui_state.scroll_detail_up_by(command.rows, command.visible_rows, command.wrap_width)
}
}
input::ScrollTarget::Help => {
if command.down {
ui_state.scroll_help_down_by(
command.rows,
command.visible_rows,
command.wrap_width,
input::help_text(),
)
} else {
ui_state.scroll_help_up_by(
command.rows,
command.visible_rows,
command.wrap_width,
input::help_text(),
)
}
}
input::ScrollTarget::SystemPrompt => {
if command.down {
ui_state.scroll_system_prompt_down_by(
command.rows,
command.visible_rows,
command.wrap_width,
)
} else {
ui_state.scroll_system_prompt_up_by(
command.rows,
command.visible_rows,
command.wrap_width,
)
}
}
input::ScrollTarget::Rewind => {
if command.down {
ui_state.scroll_rewind_down_by(
command.rows,
command.visible_rows,
command.wrap_width,
)
} else {
ui_state.scroll_rewind_up_by(command.rows, command.visible_rows, command.wrap_width)
}
}
input::ScrollTarget::Usage => {
if command.down {
ui_state.scroll_usage_down_by(
command.rows,
command.visible_rows,
command.wrap_width,
)
} else {
ui_state.scroll_usage_up_by(command.rows, command.visible_rows, command.wrap_width)
}
}
input::ScrollTarget::SessionPreview => {
if command.down {
ui_state.session_picker_preview_down_by(
command.rows,
command.visible_rows,
command.wrap_width,
)
} else {
ui_state.session_picker_preview_up_by(
command.rows,
command.visible_rows,
command.wrap_width,
)
}
}
input::ScrollTarget::SubagentViewer => {
if command.down {
ui_state.scroll_subagent_viewer_down(
command.rows,
command.visible_rows,
command.wrap_width,
)
} else {
ui_state.scroll_subagent_viewer_up(
command.rows,
command.visible_rows,
command.wrap_width,
)
}
}
input::ScrollTarget::Skills => {
let before = ui_state.scroll_offset(&ui_state.scroll_views.skills_modal);
let document_rows = ui_state
.modals
.skills_modal
.as_ref()
.map(|modal| {
modal
.rows
.chunk_by(|left, right| left.source == right.source)
.map(|rows| rows.len().saturating_add(3))
.sum::<usize>()
.saturating_sub(1)
})
.unwrap_or(0);
let max_offset = document_rows.saturating_sub(usize::from(command.visible_rows));
let next = if command.down {
before
.saturating_add(usize::from(command.rows))
.min(max_offset)
} else {
before.saturating_sub(usize::from(command.rows))
};
ui_state.set_scroll_offset(&ui_state.scroll_views.skills_modal, next);
next != before
}
};
DisplayApplyResult::changed(changed)
}
fn apply_modal_command(
command: input::ModalCommand,
ui_state: &mut state::MissionControlState,
) -> DisplayApplyResult {
let changed = match command {
input::ModalCommand::SessionPickerFocusNext => ui_state.session_picker_focus_next(),
input::ModalCommand::SessionPickerFocusPrevious => ui_state.session_picker_focus_previous(),
input::ModalCommand::SessionPickerMove {
focus,
down,
amount,
visible_rows,
preview_rows,
preview_width,
} => match focus {
state::SessionPickerFocus::List => {
let page = amount == visible_rows.max(1);
if page {
if down {
ui_state.session_picker_page_down(visible_rows)
} else {
ui_state.session_picker_page_up(visible_rows)
}
} else {
let mut changed = false;
for _ in 0..amount {
changed |= if down {
ui_state.session_picker_down(visible_rows)
} else {
ui_state.session_picker_up(visible_rows)
};
}
changed
}
}
state::SessionPickerFocus::Preview => {
let rows = amount.min(usize::from(u16::MAX)) as u16;
if down {
ui_state.session_picker_preview_down_by(rows, preview_rows, preview_width)
} else {
ui_state.session_picker_preview_up_by(rows, preview_rows, preview_width)
}
}
},
input::ModalCommand::CloseSessionPicker => ui_state.close_session_picker(),
input::ModalCommand::ThemeMove {
amount,
down,
visible_rows,
} => {
let selected = if amount == visible_rows.max(1) {
if down {
ui_state.theme_picker_page_down(visible_rows)
} else {
ui_state.theme_picker_page_up(visible_rows)
}
} else if down {
ui_state.theme_picker_down(visible_rows)
} else {
ui_state.theme_picker_up(visible_rows)
};
selected.is_some()
}
input::ModalCommand::ThemeSwitchVariant => ui_state.theme_picker_switch_variant(),
input::ModalCommand::ThemeInsertChar(ch) => {
ui_state.theme_picker_insert_char(ch);
true
}
input::ModalCommand::ThemeBackspace => {
ui_state.theme_picker_backspace();
true
}
input::ModalCommand::ThemeDelete => {
ui_state.theme_picker_delete();
true
}
input::ModalCommand::ThemeCursorLeft => {
ui_state.theme_picker_cursor_left();
true
}
input::ModalCommand::ThemeCursorRight => {
ui_state.theme_picker_cursor_right();
true
}
input::ModalCommand::ThemeCursorHome => {
ui_state.theme_picker_cursor_home();
true
}
input::ModalCommand::ThemeCursorEnd => {
ui_state.theme_picker_cursor_end();
true
}
input::ModalCommand::MoveSettings {
kind,
down,
visible_rows,
} => match kind {
input::SettingsModalKind::Skills => {
if down {
ui_state.skills_modal_down(visible_rows)
} else {
ui_state.skills_modal_up(visible_rows)
}
}
input::SettingsModalKind::Mcp => {
if down {
ui_state.mcp_modal_down(visible_rows)
} else {
ui_state.mcp_modal_up(visible_rows)
}
}
input::SettingsModalKind::Tools => {
if down {
ui_state.tools_modal_down(visible_rows)
} else {
ui_state.tools_modal_up(visible_rows)
}
}
input::SettingsModalKind::Subagents => {
if down {
ui_state.subagents_modal_down(visible_rows)
} else {
ui_state.subagents_modal_up(visible_rows)
}
}
},
input::ModalCommand::SetSettingRow { kind, key, enabled } => {
match kind {
input::SettingsModalKind::Skills => ui_state.set_skill_row_enabled(&key, enabled),
input::SettingsModalKind::Mcp => ui_state.set_mcp_server_row_enabled(&key, enabled),
input::SettingsModalKind::Tools => ui_state.set_tool_row_enabled(&key, enabled),
input::SettingsModalKind::Subagents => {
ui_state.set_subagent_profile_row_enabled(&key, enabled)
}
}
true
}
input::ModalCommand::CloseSettings(kind) => match kind {
input::SettingsModalKind::Skills => ui_state.close_skills_modal(),
input::SettingsModalKind::Mcp => ui_state.close_mcp_modal(),
input::SettingsModalKind::Tools => ui_state.close_tools_modal(),
input::SettingsModalKind::Subagents => ui_state.close_subagents_modal(),
},
input::ModalCommand::CloseSystemPrompt => ui_state.close_system_prompt_modal(),
input::ModalCommand::CloseUsage => ui_state.close_usage_modal(),
input::ModalCommand::RewindPickerMoveOrScroll { down, viewport } => {
let moved = if down {
ui_state.rewind_picker_down()
} else {
ui_state.rewind_picker_up()
};
if moved {
true
} else if down {
ui_state.scroll_rewind_down_by(1, viewport.visible_rows, viewport.wrap_width)
} else {
ui_state.scroll_rewind_up_by(1, viewport.visible_rows, viewport.wrap_width)
}
}
input::ModalCommand::CycleRewindMode => {
if let Some(modal) = ui_state.modals.rewind_modal.as_mut()
&& modal.show_mode_choices
{
modal.mode = modal.mode.next();
true
} else {
false
}
}
input::ModalCommand::CloseRewind => ui_state.close_rewind_modal(),
input::ModalCommand::CloseLogoutConfirmation => ui_state.close_logout_confirmation(),
input::ModalCommand::CloseLogoutPicker => ui_state.close_logout_picker(),
input::ModalCommand::LogoutPickerMove { down, visible_rows } => {
if down {
ui_state.logout_picker_down(visible_rows)
} else {
ui_state.logout_picker_up(visible_rows)
}
}
input::ModalCommand::CloseModelPicker => ui_state.close_model_picker(),
input::ModalCommand::ModelPickerMove { down, visible_rows } => {
if down {
ui_state.model_picker_down(visible_rows)
} else {
ui_state.model_picker_up(visible_rows)
}
}
input::ModalCommand::CloseSubagentViewer => ui_state.close_subagent_viewer(),
input::ModalCommand::SubagentViewerSelectNext => ui_state.select_next_subagent_viewer_tab(),
input::ModalCommand::SubagentViewerSelectPrevious => {
ui_state.select_previous_subagent_viewer_tab()
}
input::ModalCommand::SubagentViewerScrollHome => ui_state.home_subagent_viewer(),
input::ModalCommand::SubagentViewerScrollEnd { viewport } => {
ui_state.end_subagent_viewer(viewport.visible_rows, viewport.wrap_width)
}
};
DisplayApplyResult::changed(changed)
}
fn apply_mouse_command(
command: input::MouseCommand,
ui_state: &mut state::MissionControlState,
) -> DisplayApplyResult {
match command {
input::MouseCommand::Consume => DisplayApplyResult::changed(false),
input::MouseCommand::ClearSelection => {
DisplayApplyResult::changed(ui_state.clear_selection())
}
input::MouseCommand::StartPromptSelection { byte } => {
if ui_state.header_modal_visible() {
ui_state.focus_prompt();
}
ui_state.start_prompt_selection_at(byte);
DisplayApplyResult::changed(true)
}
input::MouseCommand::UpdatePromptSelection { byte } => {
DisplayApplyResult::changed(ui_state.update_prompt_selection_at(byte))
}
input::MouseCommand::FinishPromptSelection {
byte,
visible_rows,
wrap_width,
} => {
let copy = ui_state.finish_selection(render::TuiPane::Prompt, wrap_width);
if let Some(copy) = copy {
DisplayApplyResult {
changed: true,
copy: Some(copy),
}
} else if let Some(byte) = byte {
ui_state.move_prompt_cursor_to(byte, visible_rows, wrap_width);
DisplayApplyResult::changed(true)
} else {
DisplayApplyResult::changed(false)
}
}
input::MouseCommand::StartTranscriptSelection { position } => {
if ui_state.modals.header_modals_pinned {
ui_state.focus_transcript();
}
ui_state.start_selection(render::TuiPane::Transcript, position);
DisplayApplyResult::changed(true)
}
input::MouseCommand::UpdateTranscriptSelection { position } => DisplayApplyResult::changed(
ui_state.update_selection(render::TuiPane::Transcript, position),
),
input::MouseCommand::FinishTranscriptSelection { wrap_width } => {
let copy = ui_state.finish_selection(render::TuiPane::Transcript, wrap_width);
DisplayApplyResult {
changed: copy.is_some(),
copy,
}
}
input::MouseCommand::ClickActivityTree { row, visible_rows } => {
let selection_changed = ui_state.clear_selection();
let focus_changed = !ui_state.is_activity_tree_focused();
ui_state.focus_activity_tree();
let activity_changed = ui_state.click_activity_tree_row(row, visible_rows);
if activity_changed {
ui_state.open_activity_detail();
}
DisplayApplyResult::changed(selection_changed || focus_changed || activity_changed)
}
input::MouseCommand::ClickActivityDetail {
row,
visible_rows,
wrap_width,
} => {
let selection_changed = ui_state.clear_selection();
let metadata_clicked = ui_state
.detail_metadata_header_viewport_rows(visible_rows, wrap_width)
.is_some_and(|header_rows| header_rows.contains(&row))
&& ui_state.toggle_detail_metadata(visible_rows, wrap_width);
DisplayApplyResult::changed(selection_changed || metadata_clicked)
}
input::MouseCommand::SelectSessionRow {
visible_index,
visible_rows,
} => DisplayApplyResult::changed(
ui_state.session_picker_select_visible_row(visible_index, visible_rows),
),
input::MouseCommand::PinSubagentCard { batch_id, task_id } => {
ui_state.clear_selection();
ui_state.pin_subagent_card_task(batch_id, task_id);
DisplayApplyResult::changed(true)
}
input::MouseCommand::SelectSubagentViewerTab { visible_index } => {
DisplayApplyResult::changed(ui_state.select_subagent_viewer_tab(visible_index))
}
input::MouseCommand::OpenSubagentViewer { batch_id, task_id } => {
DisplayApplyResult::changed(ui_state.open_subagent_viewer(batch_id, task_id))
}
}
}