use super::super::*;
use super::support::*;
#[test]
fn alt_a_toggles_activity_column_without_editing_or_losing_state() {
let mut state = state_with_activity();
state.focus_activity_detail();
state.focus_activity_layout();
state.set_prompt_text("draft", 3);
state.selected = 1;
state.expanded.insert(ActivityId::new("running"));
state.set_scroll_offset(&state.scroll_views.transcript, 4);
state.set_scroll_offset(&state.scroll_views.activity_tree, 5);
state.set_scroll_offset(&state.scroll_views.detail, 6);
let before = (
state.layout_mode,
state.prompt_plain_text(),
state.prompt_cursor(),
state.selected,
state.expanded.clone(),
state.transcript_scroll_offset(),
state.activity_scroll_offset(),
state.detail_offset(),
);
assert_eq!(
handle_key(
modified_key(KeyCode::Char('a'), KeyModifiers::ALT),
&mut state,
),
InputAction::None
);
assert!(!state.activity_column_visible());
assert!(state.is_transcript_focused());
assert!(!state.prompt_cursor_visible());
assert_eq!(
(
state.layout_mode,
state.prompt_plain_text(),
state.prompt_cursor(),
state.selected,
state.expanded.clone(),
state.transcript_scroll_offset(),
state.activity_scroll_offset(),
state.detail_offset(),
),
before
);
assert_eq!(
handle_key(
modified_key(KeyCode::Char('a'), KeyModifiers::ALT),
&mut state,
),
InputAction::None
);
assert!(state.activity_column_visible());
assert!(state.is_transcript_focused());
assert_eq!(state.prompt_plain_text(), "draft");
state.focus_prompt();
state.toggle_activity_column();
assert!(!state.activity_column_visible());
assert!(state.is_prompt_focused());
handle_key(
modified_key(KeyCode::Char('a'), KeyModifiers::ALT),
&mut state,
);
assert!(state.activity_column_visible());
assert!(state.is_prompt_focused());
assert_eq!(state.prompt_plain_text(), "draft");
}
#[test]
fn removed_activity_focus_shortcuts_do_not_reveal_hidden_column() {
let mut state = state_with_activity();
state.toggle_activity_column();
assert!(!state.activity_column_visible());
assert!(state.is_prompt_focused());
for key in [
modified_key(KeyCode::Char('1'), KeyModifiers::ALT),
modified_key(KeyCode::Char('2'), KeyModifiers::ALT),
] {
assert_eq!(handle_key(key, &mut state), InputAction::None);
assert!(!state.activity_column_visible());
assert!(state.is_prompt_focused());
}
handle_key(
modified_key(KeyCode::Char('t'), KeyModifiers::CONTROL),
&mut state,
);
assert!(state.activity_column_visible());
assert!(state.is_activity_tree_focused());
}
#[test]
fn activity_layout_shortcuts_reveal_hidden_column_without_changing_focus() {
for key in [
modified_key(KeyCode::Right, KeyModifiers::CONTROL | KeyModifiers::SHIFT),
modified_key(KeyCode::Right, KeyModifiers::ALT),
] {
let mut state = state_with_activity();
state.focus_transcript();
state.toggle_activity_column();
let focus_before = state.focus_pane;
assert!(!state.activity_column_visible());
assert_eq!(handle_key(key, &mut state), InputAction::None);
assert!(state.activity_column_visible());
assert_eq!(
state.layout_mode,
crate::tui::state::TuiLayoutMode::ActivityFocused
);
assert_eq!(state.focus_pane, focus_before);
assert!(state.is_transcript_focused());
}
}
#[test]
fn alt_number_shortcuts_select_right_column_tabs() {
let mut state = state_with_activity();
state.focus_activity_detail();
assert_eq!(
handle_key(
modified_key(KeyCode::Char('2'), KeyModifiers::ALT),
&mut state
),
InputAction::None
);
assert_eq!(
state.right_column_tab,
crate::tui::state::RightColumnTab::Summary
);
assert!(state.is_prompt_focused());
assert_eq!(
handle_key(
modified_key(KeyCode::Char('1'), KeyModifiers::ALT),
&mut state
),
InputAction::None
);
assert_eq!(
state.right_column_tab,
crate::tui::state::RightColumnTab::Activity
);
assert!(state.is_prompt_focused());
}
#[test]
fn alt_one_preserves_focus_and_prompt_editing() {
use crate::tui::state::{RightColumnTab, TuiFocusPane};
for focus in [
TuiFocusPane::Prompt,
TuiFocusPane::Transcript,
TuiFocusPane::ActivityTree,
TuiFocusPane::ActivityDetail,
] {
let mut state = state_with_activity();
state.focus_pane = focus;
if matches!(focus, TuiFocusPane::Prompt | TuiFocusPane::Transcript) {
state.right_column_tab = RightColumnTab::Summary;
}
for _ in 0..2 {
handle_key(
modified_key(KeyCode::Char('1'), KeyModifiers::ALT),
&mut state,
);
assert_eq!(state.right_column_tab, RightColumnTab::Activity);
assert_eq!(state.focus_pane, focus);
}
if focus == TuiFocusPane::Prompt {
handle_key(key(KeyCode::Char('x')), &mut state);
assert_eq!(state.prompt_plain_text(), "x");
}
}
}
#[test]
fn alt_one_moves_focus_out_of_hidden_summary() {
for detail in [false, true] {
let mut state = state_with_activity();
if detail {
state.focus_activity_detail();
} else {
state.focus_activity_tree();
}
let activity_focus = state.focus_pane;
state.focus_summary();
handle_key(
modified_key(KeyCode::Char('1'), KeyModifiers::ALT),
&mut state,
);
assert_eq!(state.focus_pane, activity_focus);
}
}
#[test]
fn focus_bindings_do_not_require_printable_hotkeys() {
let mut state = MissionControlState {
focus_pane: crate::tui::state::TuiFocusPane::Prompt,
..Default::default()
};
assert!(matches!(
handle_key(
modified_key(KeyCode::Char('t'), KeyModifiers::CONTROL),
&mut state
),
InputAction::None
));
assert!(state.is_activity_tree_focused());
assert!(matches!(
handle_key(key(KeyCode::Esc), &mut state),
InputAction::None
));
assert!(state.is_prompt_focused());
}
#[test]
fn shift_tab_still_cycles_primary_agent_not_right_tab() {
let mut state = MissionControlState::default();
state.set_primary_agents(vec![crate::tui::state::PrimaryAgentEntry {
id: "tars".to_string(),
name: "TARS".to_string(),
description: "tactical unit".to_string(),
prompt: "stay alive".to_string(),
}]);
assert_eq!(
handle_key(
modified_key(KeyCode::BackTab, KeyModifiers::SHIFT),
&mut state
),
InputAction::PrimaryAgentSelectionChanged(Some("tars".to_string()))
);
}
#[test]
fn plain_tab_cycles_primary_panes_for_each_selected_tab() {
for (tab_key, expected_right_pane) in [
(
KeyCode::Char('1'),
crate::tui::state::TuiFocusPane::ActivityTree,
),
(KeyCode::Char('2'), crate::tui::state::TuiFocusPane::Summary),
] {
let mut state = state_with_activity();
handle_key(modified_key(tab_key, KeyModifiers::ALT), &mut state);
state.focus_prompt();
handle_key(key(KeyCode::Tab), &mut state);
assert!(state.is_transcript_focused());
handle_key(key(KeyCode::Tab), &mut state);
assert_eq!(state.focus_pane, expected_right_pane);
if state.is_summary_focused() {
assert!(!state.session_files.focused);
handle_key(key(KeyCode::Tab), &mut state);
assert!(state.is_summary_focused());
assert!(state.session_files.focused);
}
handle_key(key(KeyCode::Tab), &mut state);
assert!(state.is_prompt_focused());
}
}
#[test]
fn plain_tab_reveals_selected_primary_pane_when_right_column_is_hidden() {
let mut state = state_with_activity();
state.focus_transcript();
state.toggle_activity_column();
assert!(!state.activity_column_visible());
assert_eq!(handle_key(key(KeyCode::Tab), &mut state), InputAction::None);
assert!(state.activity_column_visible());
assert!(state.is_activity_tree_focused());
}
#[test]
fn alt_a_preserves_selected_tab_and_hides_or_restores_its_rail() {
let area = ratatui::layout::Rect::new(0, 0, 100, 30);
let mut state = state_with_activity();
state.focus_summary();
assert!(
crate::tui::render::layout_for(area, &state)
.pane_area(crate::tui::render::TuiPane::RightColumnRail)
.is_some()
);
handle_key(
modified_key(KeyCode::Char('a'), KeyModifiers::ALT),
&mut state,
);
assert!(!state.activity_column_visible());
assert_eq!(
state.right_column_tab,
crate::tui::state::RightColumnTab::Summary
);
assert!(
crate::tui::render::layout_for(area, &state)
.pane_area(crate::tui::render::TuiPane::RightColumnRail)
.is_none()
);
handle_key(
modified_key(KeyCode::Char('a'), KeyModifiers::ALT),
&mut state,
);
let layout = crate::tui::render::layout_for(area, &state);
assert!(state.activity_column_visible());
assert_eq!(
state.right_column_tab,
crate::tui::state::RightColumnTab::Summary
);
assert!(
layout
.pane_area(crate::tui::render::TuiPane::Summary)
.is_some()
);
assert!(
layout
.pane_area(crate::tui::render::TuiPane::RightColumnRail)
.is_some()
);
}
#[test]
fn summary_controls_scroll_without_changing_activity_selection() {
let mut state = state_with_activity();
state.focus_activity_tree();
state.selected = 1;
state
.expanded
.insert(crate::output::ActivityId::new("running"));
state.set_scroll_offset(&state.scroll_views.activity_tree, 1);
state.set_scroll_offset(&state.scroll_views.detail, 2);
state.focus_summary();
state.summary.entries = vec!["summary line\n".repeat(100)];
assert_eq!(
handle_key(key(KeyCode::Enter), &mut state),
InputAction::None
);
assert_eq!(state.summary.requested_enabled, Some(true));
assert_eq!(
handle_key(key(KeyCode::Down), &mut state),
InputAction::None
);
assert_eq!(state.summary.scroll, 1);
let before = (
state.selected,
state.expanded.clone(),
state.activity_scroll_offset(),
state.detail_offset(),
);
for code in [
KeyCode::Up,
KeyCode::Down,
KeyCode::Left,
KeyCode::Right,
KeyCode::PageUp,
KeyCode::PageDown,
] {
assert_eq!(handle_key(key(code), &mut state), InputAction::None);
}
assert_eq!(
(
state.selected,
state.expanded.clone(),
state.activity_scroll_offset(),
state.detail_offset(),
),
before
);
}
#[test]
fn summary_files_keyboard_scroll_is_independent() {
let mut state = state_with_activity();
state.focus_summary();
state.summary.entries = vec!["summary\n".repeat(100)];
for index in 0..100 {
state.session_files.record(
"read",
true,
&serde_json::json!({"path":format!("src/{index:03}.rs")}),
&[],
);
}
handle_key(key(KeyCode::Down), &mut state);
assert_eq!(state.summary.scroll, 1);
handle_key(key(KeyCode::Tab), &mut state);
assert!(state.session_files.focused);
handle_key(key(KeyCode::Down), &mut state);
assert_eq!(state.session_files.scroll, 1);
assert_eq!(state.summary.scroll, 1);
handle_key(key(KeyCode::PageDown), &mut state);
assert!(state.session_files.scroll > 1);
for _ in 0..3 {
handle_key(key(KeyCode::Tab), &mut state);
}
assert!(state.is_summary_focused());
assert!(!state.session_files.focused);
handle_key(key(KeyCode::Up), &mut state);
assert_eq!(state.summary.scroll, 0);
assert!(state.session_files.scroll > 1);
}
#[test]
fn plain_f_types_into_prompt_from_either_summary_section() {
for files in [false, true] {
let mut state = state_with_activity();
state.focus_summary();
state.session_files.focused = files;
handle_key(key(KeyCode::Char('f')), &mut state);
assert!(state.is_prompt_focused());
assert_eq!(state.prompt_plain_text(), "f");
}
}