codetether-agent 4.5.7

A2A-native AI coding agent for the CodeTether ecosystem
Documentation
use crossterm::event::KeyModifiers;

use crate::tui::app::model_picker::close_model_picker;
use crate::tui::app::session_sync::return_to_chat;
use crate::tui::app::state::App;
use crate::tui::app::symbols::symbol_search_active;
use crate::tui::models::{InputMode, ViewMode};

pub fn handle_escape(app: &mut App) {
    if symbol_search_active(app) {
        app.state.symbol_search.close();
        app.state.status = "Closed symbol search".to_string();
    } else if app.state.show_help {
        app.state.show_help = false;
        app.state.status = "Closed help".to_string();
    } else if app.state.model_picker_active {
        close_model_picker(app);
        app.state.status = "Closed model picker".to_string();
    } else {
        match app.state.view_mode {
            ViewMode::Sessions => {
                app.state.clear_session_filter();
                return_to_chat(app);
            }
            ViewMode::FilePicker => {
                app.state.file_picker_active = false;
                return_to_chat(app);
            }
            ViewMode::Swarm if app.state.swarm.detail_mode => app.state.swarm.exit_detail(),
            ViewMode::Ralph if app.state.ralph.detail_mode => app.state.ralph.exit_detail(),
            ViewMode::Bus if app.state.bus_log.filter_input_mode => {
                app.state.bus_log.exit_filter_mode();
                app.state.status = "Protocol filter closed".to_string();
            }
            ViewMode::Bus if app.state.bus_log.detail_mode => app.state.bus_log.exit_detail(),
            ViewMode::Chat => app.state.input_mode = InputMode::Normal,
            _ => return_to_chat(app),
        }
    }
}

pub fn handle_tab(app: &mut App) {
    if app.state.apply_selected_slash_suggestion() {
        app.state.status = "Command autocompleted".to_string();
    }
}

pub fn toggle_help(app: &mut App) {
    app.state.show_help = !app.state.show_help;
    app.state.help_scroll.offset = 0;
    app.state.status = if app.state.show_help {
        "Help".to_string()
    } else {
        "Closed help".to_string()
    };
}

pub fn handle_up(app: &mut App, modifiers: KeyModifiers) {
    if app.state.show_help {
        app.state.help_scroll.scroll_up(1);
        return;
    }
    if symbol_search_active(app) {
        app.state.symbol_search.select_prev();
        return;
    }
    if app.state.view_mode == ViewMode::Sessions {
        app.state.sessions_select_prev();
        return;
    }
    if app.state.view_mode == ViewMode::Model {
        app.state.model_select_prev();
        return;
    }
    if app.state.view_mode == ViewMode::Settings {
        app.state.settings_select_prev();
        return;
    }
    if app.state.slash_suggestions_visible() {
        app.state.select_prev_slash_suggestion();
        return;
    }
    if app.state.view_mode == ViewMode::Chat {
        if modifiers.contains(KeyModifiers::CONTROL) {
            let _ = app.state.history_prev();
        } else if modifiers.contains(KeyModifiers::SHIFT) {
            app.state.scroll_tool_preview_up(1);
        } else {
            app.state.scroll_up(1);
        }
        return;
    }

    match app.state.view_mode {
        ViewMode::Swarm => {
            if app.state.swarm.detail_mode {
                app.state.swarm.detail_scroll_up(1);
            } else {
                app.state.swarm.select_prev();
            }
        }
        ViewMode::Ralph => {
            if app.state.ralph.detail_mode {
                app.state.ralph.detail_scroll_up(1);
            } else {
                app.state.ralph.select_prev();
            }
        }
        ViewMode::Bus if !app.state.bus_log.filter_input_mode => {
            if app.state.bus_log.detail_mode {
                app.state.bus_log.detail_scroll_up(1);
            } else {
                app.state.bus_log.select_prev();
            }
        }
        _ => app.state.scroll_up(1),
    }
}

pub fn handle_down(app: &mut App, modifiers: KeyModifiers) {
    if app.state.show_help {
        app.state.help_scroll.scroll_down(1, 200);
        return;
    }
    if symbol_search_active(app) {
        app.state.symbol_search.select_next();
        return;
    }
    if app.state.view_mode == ViewMode::Sessions {
        app.state.sessions_select_next();
        return;
    }
    if app.state.view_mode == ViewMode::Model {
        app.state.model_select_next();
        return;
    }
    if app.state.view_mode == ViewMode::Settings {
        app.state.settings_select_next();
        return;
    }
    if app.state.slash_suggestions_visible() {
        app.state.select_next_slash_suggestion();
        return;
    }
    if app.state.view_mode == ViewMode::Chat {
        if modifiers.contains(KeyModifiers::CONTROL) {
            let _ = app.state.history_next();
        } else if modifiers.contains(KeyModifiers::SHIFT) {
            app.state.scroll_tool_preview_down(1);
        } else {
            app.state.scroll_down(1);
        }
        return;
    }

    match app.state.view_mode {
        ViewMode::Swarm => {
            if app.state.swarm.detail_mode {
                app.state.swarm.detail_scroll_down(1);
            } else {
                app.state.swarm.select_next();
            }
        }
        ViewMode::Ralph => {
            if app.state.ralph.detail_mode {
                app.state.ralph.detail_scroll_down(1);
            } else {
                app.state.ralph.select_next();
            }
        }
        ViewMode::Bus if !app.state.bus_log.filter_input_mode => {
            if app.state.bus_log.detail_mode {
                app.state.bus_log.detail_scroll_down(1);
            } else {
                app.state.bus_log.select_next();
            }
        }
        _ => app.state.scroll_down(1),
    }
}

pub fn handle_page_up(app: &mut App) {
    if app.state.show_help {
        app.state.help_scroll.scroll_up(10);
        return;
    }

    match app.state.view_mode {
        ViewMode::Swarm if app.state.swarm.detail_mode => app.state.swarm.detail_scroll_up(10),
        ViewMode::Ralph if app.state.ralph.detail_mode => app.state.ralph.detail_scroll_up(10),
        ViewMode::Bus if app.state.bus_log.detail_mode => app.state.bus_log.detail_scroll_up(10),
        ViewMode::Chat => app.state.scroll_up(10),
        _ => {}
    }
}

pub fn handle_page_down(app: &mut App) {
    if app.state.show_help {
        app.state.help_scroll.scroll_down(10, 200);
        return;
    }

    match app.state.view_mode {
        ViewMode::Swarm if app.state.swarm.detail_mode => app.state.swarm.detail_scroll_down(10),
        ViewMode::Ralph if app.state.ralph.detail_mode => app.state.ralph.detail_scroll_down(10),
        ViewMode::Bus if app.state.bus_log.detail_mode => app.state.bus_log.detail_scroll_down(10),
        ViewMode::Chat => app.state.scroll_down(10),
        _ => {}
    }
}

pub fn handle_home(app: &mut App) {
    if app.state.view_mode == ViewMode::Chat {
        app.state.move_cursor_home();
    }
}

pub fn handle_end(app: &mut App) {
    if app.state.view_mode == ViewMode::Chat {
        app.state.move_cursor_end();
    }
}

pub fn handle_left(app: &mut App, modifiers: KeyModifiers) {
    if app.state.view_mode == ViewMode::Chat {
        if modifiers.contains(KeyModifiers::CONTROL) {
            app.state.move_cursor_word_left();
        } else {
            app.state.move_cursor_left();
        }
    }
}

pub fn handle_right(app: &mut App, modifiers: KeyModifiers) {
    if app.state.view_mode == ViewMode::Chat {
        if modifiers.contains(KeyModifiers::CONTROL) {
            app.state.move_cursor_word_right();
        } else {
            app.state.move_cursor_right();
        }
    }
}

pub fn handle_delete(app: &mut App) {
    if app.state.view_mode == ViewMode::Chat {
        app.state.delete_forward();
    }
}

pub fn handle_symbol_enter(app: &mut App) {
    if let Some(symbol) = app.state.symbol_search.selected_symbol() {
        app.state.status = format!(
            "Selected symbol {} {}",
            symbol.name,
            symbol
                .line
                .map(|line| format!("at line {line}"))
                .unwrap_or_default()
        );
    }
    app.state.symbol_search.close();
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn help_overlay_consumes_chat_arrow_navigation() {
        let mut app = App::default();
        app.state.show_help = true;
        app.state.set_view_mode(ViewMode::Chat);
        app.state.set_chat_max_scroll(25);
        app.state.scroll_to_bottom();

        handle_down(&mut app, KeyModifiers::NONE);

        assert_eq!(app.state.help_scroll.offset, 1);
        assert_eq!(app.state.chat_scroll, 1_000_000);
    }
}