agentmux 0.4.0

Multi-agent coordination runtime with inter-agent messaging across CLI, MCP, tmux, and ACP.
Documentation
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseEventKind};

use crate::runtime::error::RuntimeError;

use super::state::{AppState, FocusField, ScreenMode};

pub(crate) fn handle_event(state: &mut AppState, event: Event) -> Result<(), RuntimeError> {
    match event {
        Event::Key(key) => handle_key(state, key),
        Event::Mouse(mouse) => {
            match mouse.kind {
                MouseEventKind::ScrollUp => state.scroll_chat_history_up(),
                MouseEventKind::ScrollDown => state.scroll_chat_history_down(),
                _ => {}
            }
            Ok(())
        }
        Event::Paste(text) => {
            insert_text_for_active_mode(state, text.as_str());
            Ok(())
        }
        _ => Ok(()),
    }
}

fn handle_key(state: &mut AppState, key: KeyEvent) -> Result<(), RuntimeError> {
    if key.kind != KeyEventKind::Press {
        return Ok(());
    }

    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
        state.should_quit = true;
        return Ok(());
    }

    if key.code == KeyCode::F(1) {
        state.toggle_help_overlay();
        return Ok(());
    }

    if state.picker_open {
        return handle_picker_key(state, key);
    }
    if state.events_overlay_open {
        return handle_events_overlay_key(state, key);
    }
    if state.help_overlay_open {
        return handle_help_overlay_key(state, key);
    }

    if key.code == KeyCode::F(4) {
        state.toggle_mode();
        return Ok(());
    }

    match state.mode {
        ScreenMode::Communication => handle_communication_mode_key(state, key),
        ScreenMode::Interaction => handle_interaction_mode_key(state, key),
    }
}

fn handle_communication_mode_key(state: &mut AppState, key: KeyEvent) -> Result<(), RuntimeError> {
    if key.modifiers.contains(KeyModifiers::CONTROL) {
        match key.code {
            KeyCode::Char('a') if state.focus == FocusField::Message => {
                state.move_message_cursor_home();
                return Ok(());
            }
            KeyCode::Char('e') if state.focus == FocusField::Message => {
                state.move_message_cursor_end();
                return Ok(());
            }
            KeyCode::Char('j') => {
                state.insert_newline_if_message();
                return Ok(());
            }
            KeyCode::Char('r') => return state.refresh_recipients(),
            KeyCode::Char(' ') => state.autocomplete_active_recipient_field(),
            _ => {}
        }
    }

    match key.code {
        KeyCode::F(2) => {
            if state.picker_open {
                state.close_picker();
            } else {
                state.open_picker();
            }
        }
        KeyCode::F(3) => state.toggle_events_overlay(),
        KeyCode::BackTab => state.cycle_focus_backward(),
        KeyCode::Tab => state.cycle_focus_forward(),
        KeyCode::Enter if key.modifiers.is_empty() => {
            if state.focus == FocusField::To {
                state.accept_active_to_completion();
            } else if state.focus == FocusField::Message {
                return state.send_message();
            }
        }
        KeyCode::Esc if state.focus == FocusField::Message => {
            state.snap_chat_history_to_latest();
        }
        KeyCode::Up => {
            if state.focus == FocusField::To {
                state.move_to_completion_selection(-1);
            } else if state.focus == FocusField::Message {
                state.move_message_cursor_up();
            }
        }
        KeyCode::Down => {
            if state.focus == FocusField::To {
                state.move_to_completion_selection(1);
            } else if state.focus == FocusField::Message {
                state.move_message_cursor_down();
            }
        }
        KeyCode::Left if state.focus == FocusField::Message => {
            state.move_message_cursor_left();
        }
        KeyCode::Right if state.focus == FocusField::Message => {
            state.move_message_cursor_right();
        }
        KeyCode::Home if state.focus == FocusField::Message => {
            state.move_message_cursor_home();
        }
        KeyCode::End if state.focus == FocusField::Message => {
            state.move_message_cursor_end();
        }
        KeyCode::Backspace => state.backspace(),
        KeyCode::PageUp => state.scroll_chat_history_page_up(),
        KeyCode::PageDown => state.scroll_chat_history_page_down(),
        KeyCode::Char(character)
            if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT =>
        {
            state.insert_character(character);
        }
        _ => {}
    }

    Ok(())
}

fn handle_interaction_mode_key(state: &mut AppState, key: KeyEvent) -> Result<(), RuntimeError> {
    if key.modifiers.contains(KeyModifiers::CONTROL) {
        match key.code {
            KeyCode::Char('j') => {
                state.insert_newline_in_raww();
                return Ok(());
            }
            KeyCode::Char('r') => return state.refresh_recipients(),
            _ => {}
        }
    }

    match key.code {
        KeyCode::F(2) => state.open_picker(),
        KeyCode::F(3) => state.toggle_events_overlay(),
        KeyCode::PageUp => state.scroll_interaction_snapshot_page_up(),
        KeyCode::PageDown => state.scroll_interaction_snapshot_page_down(),
        KeyCode::Left => {
            if interaction_permission_active(state) {
                state.move_look_permission_request_selection(-1);
            } else {
                state.move_raww_cursor_left();
            }
        }
        KeyCode::Right => {
            if interaction_permission_active(state) {
                state.move_look_permission_request_selection(1);
            } else {
                state.move_raww_cursor_right();
            }
        }
        KeyCode::Up => {
            if interaction_permission_active(state) {
                state.move_look_permission_option_selection(-1);
            } else if !state.raww_draft.is_empty() {
                state.move_raww_cursor_up();
            } else {
                state.scroll_interaction_snapshot_up();
            }
        }
        KeyCode::Down => {
            if interaction_permission_active(state) {
                state.move_look_permission_option_selection(1);
            } else if !state.raww_draft.is_empty() {
                state.move_raww_cursor_down();
            } else {
                state.scroll_interaction_snapshot_down();
            }
        }
        KeyCode::Home if !interaction_permission_active(state) => {
            state.move_raww_cursor_home();
        }
        KeyCode::End if !interaction_permission_active(state) => {
            state.move_raww_cursor_end();
        }
        KeyCode::Enter => {
            if interaction_permission_active(state) {
                return state.resolve_selected_look_permission_selected();
            }
            return state.dispatch_raww_from_interaction();
        }
        KeyCode::Backspace => state.backspace_raww(),
        KeyCode::Char(character)
            if interaction_permission_active(state)
                && (character == 'c' || character == 'C')
                && (key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT) =>
        {
            return state.resolve_selected_look_permission_cancelled();
        }
        KeyCode::Char(character)
            if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT =>
        {
            state.insert_character_in_raww(character);
        }
        _ => {}
    }

    Ok(())
}

fn interaction_permission_active(state: &AppState) -> bool {
    state.raww_draft.is_empty() && !state.look_pending_permissions().is_empty()
}

fn insert_text_for_active_mode(state: &mut AppState, text: &str) {
    match state.mode {
        ScreenMode::Communication => state.insert_text(text),
        ScreenMode::Interaction => {
            for character in text.chars() {
                state.insert_character_in_raww(character);
            }
        }
    }
}

fn handle_picker_key(state: &mut AppState, key: KeyEvent) -> Result<(), RuntimeError> {
    match key.code {
        KeyCode::Esc | KeyCode::F(2) => state.close_picker(),
        KeyCode::F(3) => {
            state.close_picker();
            state.toggle_events_overlay();
        }
        KeyCode::F(4) => {
            state.close_picker();
            state.toggle_mode();
        }
        KeyCode::Down => state.move_picker_selection(1),
        KeyCode::Up => state.move_picker_selection(-1),
        KeyCode::Enter => state.insert_picker_selection(),
        KeyCode::Char(character)
            if (character == 'l' || character == 'L')
                && (key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT) =>
        {
            return state.look_picker_target();
        }
        KeyCode::Char(character)
            if (character == 'w' || character == 'W')
                && (key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT) =>
        {
            return state.raww_picker_target();
        }
        _ => {}
    }
    Ok(())
}

fn handle_events_overlay_key(state: &mut AppState, key: KeyEvent) -> Result<(), RuntimeError> {
    match key.code {
        KeyCode::Esc | KeyCode::F(3) => state.toggle_events_overlay(),
        KeyCode::F(2) => {
            state.toggle_events_overlay();
            state.open_picker();
        }
        KeyCode::F(4) => {
            state.toggle_events_overlay();
            state.toggle_mode();
        }
        _ => {}
    }
    Ok(())
}

fn handle_help_overlay_key(state: &mut AppState, key: KeyEvent) -> Result<(), RuntimeError> {
    match key.code {
        KeyCode::Esc | KeyCode::F(1) => state.toggle_help_overlay(),
        KeyCode::F(2) => {
            state.toggle_help_overlay();
            state.open_picker();
        }
        KeyCode::F(3) => {
            state.toggle_help_overlay();
            state.toggle_events_overlay();
        }
        KeyCode::F(4) => {
            state.toggle_help_overlay();
            state.toggle_mode();
        }
        _ => {}
    }
    Ok(())
}