magi-code 0.82.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::super::*;
use super::support::*;

#[test]
fn alt_a_works_with_help_but_modal_precedence_blocks_it() {
    let mut state = state_with_activity();
    state.show_help = true;
    handle_key(
        modified_key(KeyCode::Char('a'), KeyModifiers::ALT),
        &mut state,
    );
    assert!(!state.activity_column_visible());
    assert!(state.show_help);

    state.open_system_prompt_modal("modal content".to_string());
    handle_key(
        modified_key(KeyCode::Char('a'), KeyModifiers::ALT),
        &mut state,
    );
    assert!(!state.activity_column_visible());
    assert!(state.system_prompt_modal_visible());
}

#[test]
fn system_prompt_modal_alt_arrows_scroll_modal_and_suppress_underlying_state() {
    let mut state = state_with_activity();
    state.focus_prompt();
    state.set_prompt_text("line one\nline two", "line one\nline two".len());
    state.set_scroll_offset(&state.scroll_views.transcript, 2);
    state.set_scroll_offset(&state.scroll_views.activity_tree, 3);
    state.set_scroll_offset(&state.scroll_views.detail, 4);
    state.open_system_prompt_modal(
        (0..12)
            .map(|line| format!("modal line {line}"))
            .collect::<Vec<_>>()
            .join("\n"),
    );
    let viewport = Some(SystemPromptViewport {
        visible_rows: 3,
        wrap_width: 80,
    });

    assert!(matches!(
        handle_key_with_all_viewports(
            modified_key(KeyCode::Down, KeyModifiers::ALT),
            &mut state,
            KeyViewports {
                system_prompt: viewport,
                ..KeyViewports::default()
            },
            &[]
        ),
        InputAction::None
    ));
    assert_eq!(state.scroll_offset(&state.scroll_views.system_prompt), 1);
    assert_eq!(state.prompt_cursor(), "line one\nline two".len());
    assert_eq!(state.transcript_scroll_offset(), 2);
    assert_eq!(state.activity_scroll_offset(), 3);
    assert_eq!(state.detail_offset(), 4);

    assert!(matches!(
        handle_key_with_all_viewports(
            modified_key(KeyCode::Up, KeyModifiers::ALT),
            &mut state,
            KeyViewports {
                system_prompt: viewport,
                ..KeyViewports::default()
            },
            &[]
        ),
        InputAction::None
    ));
    assert_eq!(state.scroll_offset(&state.scroll_views.system_prompt), 0);
    assert_eq!(state.prompt_cursor(), "line one\nline two".len());
    assert_eq!(state.transcript_scroll_offset(), 2);
    assert_eq!(state.activity_scroll_offset(), 3);
    assert_eq!(state.detail_offset(), 4);
}

fn theme_picker_state() -> MissionControlState {
    use crate::appearance::ThemeCatalogEntry;
    use opaline::ThemeVariant::{Dark, Light};

    let entries = [
        ("dark-one", "Shared dark", Dark),
        ("light-one", "Shared light", Light),
        ("dark-two", "Other dark", Dark),
        ("light-two", "Other light", Light),
    ]
    .into_iter()
    .map(|(id, display_name, variant)| ThemeCatalogEntry {
        id: id.into(),
        display_name: display_name.into(),
        variant,
    })
    .collect();
    let mut state = MissionControlState::default();
    state.open_theme_picker("dark-one".into(), 1, false, None, entries, None);
    state
}

fn apply_theme_picker_key(key: KeyEvent, state: &mut MissionControlState) -> (InputAction, bool) {
    let command = classify_key_at(
        key,
        state,
        KeyViewports {
            theme_picker: Some(PickerViewport { visible_rows: 2 }),
            theme_query_visible: true,
            ..Default::default()
        },
        &[],
        Instant::now(),
    );
    let (action, display) = command.into_parts();
    let changed = display.is_some_and(|command| {
        crate::tui::controller::apply_display_command_for_test(command, state, &[]).0
    });
    if let InputAction::PreviewTheme(id) = &action {
        assert_eq!(
            state.theme_picker_selected_id().as_deref(),
            Some(id.as_str())
        );
    }
    (action, changed)
}

#[test]
fn theme_picker_tab_search_and_navigation_preview_the_selected_category() {
    let mut state = theme_picker_state();
    for (code, expected) in [
        (KeyCode::Down, "dark-two"),
        (KeyCode::Up, "dark-one"),
        (KeyCode::PageDown, "dark-two"),
        (KeyCode::PageUp, "dark-one"),
    ] {
        assert!(
            matches!(apply_theme_picker_key(key(code), &mut state).0, InputAction::PreviewTheme(id) if id == expected)
        );
    }
    for ch in "shared".chars() {
        assert!(
            matches!(apply_theme_picker_key(key(KeyCode::Char(ch)), &mut state).0, InputAction::PreviewTheme(id) if id == "dark-one")
        );
    }
    for (event, expected) in [
        (key(KeyCode::Tab), "light-one"),
        (
            modified_key(KeyCode::BackTab, KeyModifiers::SHIFT),
            "dark-one",
        ),
        (modified_key(KeyCode::Tab, KeyModifiers::SHIFT), "light-one"),
        (key(KeyCode::BackTab), "dark-one"),
    ] {
        assert!(
            matches!(apply_theme_picker_key(event, &mut state).0, InputAction::PreviewTheme(id) if id == expected)
        );
        let picker = state.modals.theme_picker.as_ref().unwrap();
        assert_eq!(picker.query, "shared");
        assert_eq!(picker.query_cursor, 6);
        assert_eq!(picker.filtered_indices.len(), 1);
    }
}

#[test]
fn theme_picker_empty_results_redraw_without_preview_or_save_and_keep_cursor_keys() {
    let mut state = theme_picker_state();
    for ch in "dark".chars() {
        apply_theme_picker_key(key(KeyCode::Char(ch)), &mut state);
    }
    let (action, changed) = apply_theme_picker_key(key(KeyCode::Tab), &mut state);
    assert!(changed);
    assert!(matches!(action, InputAction::None));
    assert!(state.theme_picker_selected_id().is_none());
    assert_eq!(state.theme_picker_preview_id(), Some("dark-one"));
    assert!(matches!(
        apply_theme_picker_key(key(KeyCode::Enter), &mut state).0,
        InputAction::None
    ));

    for (code, cursor) in [
        (KeyCode::Home, 0),
        (KeyCode::Right, 1),
        (KeyCode::End, 4),
        (KeyCode::Left, 3),
    ] {
        apply_theme_picker_key(key(code), &mut state);
        let picker = state.modals.theme_picker.as_ref().unwrap();
        assert_eq!(picker.query_cursor, cursor);
        assert_eq!(picker.active_variant, opaline::ThemeVariant::Light);
    }
    for code in [KeyCode::Char('界'), KeyCode::Backspace, KeyCode::Delete] {
        let (action, changed) = apply_theme_picker_key(key(code), &mut state);
        assert!(
            changed,
            "{code:?} must repaint the search even with no selection"
        );
        assert!(matches!(action, InputAction::None));
        assert_eq!(state.theme_picker_preview_id(), Some("dark-one"));
    }
    assert_eq!(state.modals.theme_picker.as_ref().unwrap().query, "dar");
    assert!(
        matches!(apply_theme_picker_key(key(KeyCode::Tab), &mut state).0, InputAction::PreviewTheme(id) if id == "dark-one")
    );
    assert!(matches!(
        apply_theme_picker_key(key(KeyCode::Enter), &mut state).0,
        InputAction::ConfirmTheme
    ));
    assert!(matches!(
        apply_theme_picker_key(key(KeyCode::Esc), &mut state).0,
        InputAction::CancelTheme
    ));
}

#[test]
fn theme_picker_loading_and_pending_save_block_tab_changes() {
    let mut state = theme_picker_state();
    state.set_theme_picker_save_pending();
    let before = state.modals.theme_picker.clone();
    for code in [
        KeyCode::Tab,
        KeyCode::BackTab,
        KeyCode::Down,
        KeyCode::Char('x'),
        KeyCode::Enter,
        KeyCode::Esc,
    ] {
        assert!(matches!(
            apply_theme_picker_key(key(code), &mut state).0,
            InputAction::None
        ));
        assert_eq!(state.modals.theme_picker, before);
    }
    state.set_theme_picker_save_failed("disk full".into());
    assert!(
        matches!(apply_theme_picker_key(key(KeyCode::Tab), &mut state).0, InputAction::PreviewTheme(id) if id == "light-one")
    );

    state.open_theme_picker("light-one".into(), 1, true, Some(2), Vec::new(), None);
    for code in [
        KeyCode::Tab,
        KeyCode::BackTab,
        KeyCode::Char('x'),
        KeyCode::Down,
        KeyCode::Enter,
    ] {
        assert!(matches!(
            apply_theme_picker_key(key(code), &mut state).0,
            InputAction::None
        ));
        let picker = state.modals.theme_picker.as_ref().unwrap();
        assert!(picker.query.is_empty());
        assert_eq!(picker.active_variant, opaline::ThemeVariant::Dark);
        assert!(picker.loading);
    }
    assert!(matches!(
        apply_theme_picker_key(key(KeyCode::Esc), &mut state).0,
        InputAction::CancelTheme
    ));
}