vtcode-tui 0.98.4

Reusable TUI primitives and session API for VT Code-style terminal interfaces
use super::super::*;
use super::helpers::*;

#[test]
fn down_opens_local_agents_drawer_when_input_is_empty() {
    let mut session = app_session_with_input("", 0);
    session.handle_command(app_types::InlineCommand::SetLocalAgents {
        entries: vec![sample_local_agent_entry(
            app_types::LocalAgentKind::Delegated,
        )],
    });
    session.close_transient();

    let event = session.process_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));

    assert!(event.is_none());
    assert!(session.local_agents_visible());
}

#[test]
fn new_local_agent_auto_opens_drawer() {
    let mut session = app_session_with_input("", 0);

    session.handle_command(app_types::InlineCommand::SetLocalAgents {
        entries: vec![sample_local_agent_entry(
            app_types::LocalAgentKind::Delegated,
        )],
    });

    assert!(session.local_agents_visible());
}

#[test]
fn local_agents_drawer_hides_input_while_open() {
    let mut session = app_session_with_input("draft command", "draft command".len());

    session.handle_command(app_types::InlineCommand::SetLocalAgents {
        entries: vec![sample_local_agent_entry(
            app_types::LocalAgentKind::Delegated,
        )],
    });

    assert!(session.local_agents_visible());
    assert!(!session.core.input_enabled());
    assert!(
        !session
            .core
            .build_input_widget_data(VIEW_WIDTH, 1)
            .cursor_should_be_visible
    );

    let lines = rendered_app_session_lines(&mut session, 20);
    assert!(session.core.input_area().is_none());
    assert!(session.core.bottom_panel_area().is_some());
    assert!(
        lines.iter().any(|line| line.contains("Local Agents")),
        "drawer should still render"
    );
    assert!(
        !lines.iter().any(|line| line.contains("draft command")),
        "hidden composer should not render draft text"
    );
}

#[test]
fn closing_local_agents_drawer_restores_input_and_draft() {
    let mut session = app_session_with_input("draft command", "draft command".len());

    session.handle_command(app_types::InlineCommand::SetLocalAgents {
        entries: vec![sample_local_agent_entry(
            app_types::LocalAgentKind::Delegated,
        )],
    });
    let _ = rendered_app_session_lines(&mut session, 20);

    let event = session.process_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));

    assert!(event.is_none());
    assert!(!session.local_agents_visible());
    assert!(session.core.input_enabled());
    assert!(
        session
            .core
            .build_input_widget_data(VIEW_WIDTH, 1)
            .cursor_should_be_visible
    );
    assert_eq!(session.core.input_manager.content(), "draft command");

    let lines = rendered_app_session_lines(&mut session, 20);
    assert!(session.core.input_area().is_some());
    assert!(
        lines.iter().any(|line| line.contains("draft command")),
        "composer should re-render its preserved draft"
    );
}

#[test]
fn local_agents_drawer_navigation_works_with_existing_draft() {
    let mut session = app_session_with_input("draft command", "draft command".len());

    session.handle_command(app_types::InlineCommand::SetLocalAgents {
        entries: vec![
            sample_local_agent_entry_with_id(
                "agent-1",
                "rust-engineer",
                app_types::LocalAgentKind::Delegated,
            ),
            sample_local_agent_entry_with_id(
                "agent-2",
                "qa-reviewer",
                app_types::LocalAgentKind::Delegated,
            ),
        ],
    });

    let down = session.process_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
    assert!(down.is_none());

    let enter = session.process_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
    assert!(matches!(
        enter,
        Some(app_types::InlineEvent::Submit(value)) if value == "/agent inspect agent-2"
    ));
    assert_eq!(session.core.input_manager.content(), "draft command");
}

#[test]
fn new_background_local_agent_does_not_auto_open_drawer() {
    let mut session = app_session_with_input("", 0);

    session.handle_command(app_types::InlineCommand::SetLocalAgents {
        entries: vec![sample_local_agent_entry(
            app_types::LocalAgentKind::Background,
        )],
    });

    assert!(!session.local_agents_visible());
}

#[test]
fn empty_local_agents_drawer_stays_open_after_last_entry_is_removed() {
    let mut session = app_session_with_input("", 0);
    session.handle_command(app_types::InlineCommand::SetLocalAgents {
        entries: vec![sample_local_agent_entry(
            app_types::LocalAgentKind::Delegated,
        )],
    });

    session.handle_command(app_types::InlineCommand::SetLocalAgents { entries: vec![] });

    assert!(session.local_agents_visible());

    let lines = rendered_app_session_lines(&mut session, 20);
    assert!(
        lines
            .iter()
            .any(|line| line.contains("No local agents yet")),
        "drawer should remain visible and show the empty state"
    );
}

#[test]
fn alt_s_remains_subprocesses_entrypoint() {
    let mut session = app_session_with_input("", 0);

    let event = session.process_key(KeyEvent::new(KeyCode::Char('s'), KeyModifiers::ALT));

    assert!(matches!(
        event,
        Some(app_types::InlineEvent::Submit(value)) if value == "/subprocesses"
    ));
}

#[test]
fn header_suggestions_include_subagent_shortcuts() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    session.local_agents = vec![sample_local_agent_entry(
        app_types::LocalAgentKind::Delegated,
    )];

    let line = session
        .header_suggestions_line()
        .expect("header suggestions line");
    let rendered = line
        .spans
        .iter()
        .map(|span| span.content.as_ref())
        .collect::<String>();

    assert!(rendered.contains("Alt+S"));
    assert!(rendered.contains("Ctrl+B"));
}

#[test]
fn header_suggestions_hide_subagent_shortcuts_without_agents() {
    let session = Session::new(InlineTheme::default(), None, VIEW_ROWS);

    let line = session
        .header_suggestions_line()
        .expect("header suggestions line");
    let rendered = line
        .spans
        .iter()
        .map(|span| span.content.as_ref())
        .collect::<String>();

    assert!(!rendered.contains("Alt+S"));
    assert!(!rendered.contains("Ctrl+B"));
}

#[test]
fn header_suggestions_hide_subagent_shortcuts_with_background_only() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    session.local_agents = vec![sample_local_agent_entry(
        app_types::LocalAgentKind::Background,
    )];

    let line = session
        .header_suggestions_line()
        .expect("header suggestions line");
    let rendered = line
        .spans
        .iter()
        .map(|span| span.content.as_ref())
        .collect::<String>();

    assert!(!rendered.contains("Alt+S"));
    assert!(!rendered.contains("Ctrl+B"));
}

#[test]
fn empty_input_status_shows_subagent_shortcuts() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    session.local_agents = vec![sample_local_agent_entry(
        app_types::LocalAgentKind::Delegated,
    )];

    let line = session
        .render_input_status_line(VIEW_WIDTH)
        .expect("input status line");
    let rendered = line
        .spans
        .iter()
        .map(|span| span.content.as_ref())
        .collect::<String>();

    assert!(rendered.contains("Alt+S"));
    assert!(rendered.contains("Ctrl+B"));
}

#[test]
fn empty_input_status_hides_subagent_shortcuts_without_agents() {
    let session = Session::new(InlineTheme::default(), None, VIEW_ROWS);

    let rendered = session
        .render_input_status_line(VIEW_WIDTH)
        .map(|line| {
            line.spans
                .iter()
                .map(|span| span.content.as_ref())
                .collect::<String>()
        })
        .unwrap_or_default();

    assert!(!rendered.contains("Alt+S"));
    assert!(!rendered.contains("Ctrl+B"));
}

#[test]
fn empty_input_status_hides_subagent_shortcuts_with_background_only() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    session.local_agents = vec![sample_local_agent_entry(
        app_types::LocalAgentKind::Background,
    )];

    let rendered = session
        .render_input_status_line(VIEW_WIDTH)
        .map(|line| {
            line.spans
                .iter()
                .map(|span| span.content.as_ref())
                .collect::<String>()
        })
        .unwrap_or_default();

    assert!(!rendered.contains("Alt+S"));
    assert!(!rendered.contains("Ctrl+B"));
}

#[test]
fn active_subagent_input_border_adds_extra_height() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    session.header_context.subagent_badges = vec![InlineHeaderBadge {
        text: "rust-engineer".to_string(),
        style: InlineTextStyle {
            color: Some(AnsiColorEnum::Rgb(RgbColor(0xFF, 0xFF, 0xFF))),
            bg_color: Some(AnsiColorEnum::Rgb(RgbColor(0x4F, 0x8F, 0xD8))),
            ..InlineTextStyle::default()
        },
        full_background: true,
    }];

    assert_eq!(session.input_block_extra_height(), 2);
}

#[test]
fn header_shows_active_subagent_badge_with_full_background() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    session.header_context.subagent_badges = vec![InlineHeaderBadge {
        text: "rust-engineer".to_string(),
        style: InlineTextStyle {
            color: Some(AnsiColorEnum::Rgb(RgbColor(0xFF, 0xFF, 0xFF))),
            bg_color: Some(AnsiColorEnum::Rgb(RgbColor(0x4F, 0x8F, 0xD8))),
            ..InlineTextStyle::default()
        },
        full_background: true,
    }];

    let line = session.header_meta_line();
    let badge_span = line
        .spans
        .iter()
        .find(|span| span.content.as_ref() == " rust-engineer ")
        .expect("subagent badge span");

    assert_eq!(badge_span.style.fg, Some(Color::Rgb(0xFF, 0xFF, 0xFF)));
    assert_eq!(badge_span.style.bg, Some(Color::Rgb(0x4F, 0x8F, 0xD8)));
    assert!(badge_span.style.add_modifier.contains(Modifier::BOLD));
}

#[test]
fn input_block_shows_active_subagent_title_with_badge_style() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    session.set_input("review current code".to_string());
    session.header_context.subagent_badges = vec![InlineHeaderBadge {
        text: "rust-engineer".to_string(),
        style: InlineTextStyle {
            color: Some(AnsiColorEnum::Rgb(RgbColor(0xFF, 0xFF, 0xFF))),
            bg_color: Some(AnsiColorEnum::Rgb(RgbColor(0x4F, 0x8F, 0xD8))),
            ..InlineTextStyle::default()
        },
        full_background: true,
    }];

    let title = session
        .active_subagent_input_title()
        .expect("active subagent input title");
    let span = title.spans.first().expect("title span");
    assert_eq!(span.content.as_ref(), " rust-engineer ");
    assert_eq!(span.style.fg, Some(Color::Rgb(0xFF, 0xFF, 0xFF)));
    assert_eq!(span.style.bg, Some(Color::Rgb(0x4F, 0x8F, 0xD8)));
    assert!(span.style.add_modifier.contains(Modifier::BOLD));

    assert_eq!(session.input_block_extra_height(), 2);
}

#[test]
fn header_suggestions_do_not_show_memory_shortcut_when_enabled() {
    let mut session = Session::new(InlineTheme::default(), None, VIEW_ROWS);
    session.header_context.persistent_memory = Some(InlineHeaderStatusBadge {
        text: "Memory: auto".to_string(),
        tone: InlineHeaderStatusTone::Ready,
    });

    let line = session
        .header_suggestions_line()
        .expect("header suggestions line");
    let summary = line_text(&line);

    assert!(!summary.contains("/memory"));
}