cflx 0.6.153

Conflux – a spec-driven parallel coding orchestrator that runs AI agents on git worktrees
use crate::tui::events::{LogEntry, TuiCommand};
use crate::tui::types::AppMode;

use super::{guards, processing_logic, AppState, ChangeState};

pub(super) fn can_bulk_toggle_change(
    mode: AppMode,
    parallel_mode: bool,
    change: &ChangeState,
) -> bool {
    if matches!(mode, AppMode::Running) && change.is_active_display_status() {
        return false;
    }

    guards::validate_change_toggleable(
        change.is_parallel_eligible,
        parallel_mode,
        &change.display_status_cache,
        &change.id,
    )
    .is_allowed()
}

pub(super) fn toggle_selection(state: &mut AppState) -> Option<TuiCommand> {
    if state.changes.is_empty() || state.cursor_index >= state.changes.len() {
        return None;
    }

    {
        let change = &state.changes[state.cursor_index];
        if let guards::ToggleGuardResult::Blocked(msg) = guards::validate_change_toggleable(
            change.is_parallel_eligible,
            state.parallel_mode,
            &change.display_status_cache,
            &change.id,
        ) {
            state.warning_message = Some(msg);
            return None;
        }
    }

    let mode = state.mode.clone();
    let mut new_change_count = state.new_change_count;
    let result = {
        let change = &mut state.changes[state.cursor_index];
        match mode {
            AppMode::Select => guards::handle_toggle_select_mode(change, &mut new_change_count),
            AppMode::Running => guards::handle_toggle_running_mode(change, &mut new_change_count),
            AppMode::Stopped => guards::handle_toggle_stopped_mode(change, &mut new_change_count),
            AppMode::Stopping
            | AppMode::Error
            | AppMode::ConfirmWorktreeDelete
            | AppMode::QrPopup
            | AppMode::ConfirmForceKill { .. } => return None,
        }
    };
    state.new_change_count = new_change_count;

    dispatch_toggle_result(state, result)
}

fn dispatch_toggle_result(
    state: &mut AppState,
    result: guards::ToggleActionResult,
) -> Option<TuiCommand> {
    match result {
        guards::ToggleActionResult::StateOnly(log_msg) => {
            if let Some(msg) = log_msg {
                state.add_log(LogEntry::info(msg));
            }
            None
        }
        guards::ToggleActionResult::Command(cmd, log_msg) => {
            if let Some(msg) = log_msg {
                state.add_log(LogEntry::info(msg));
            }
            Some(cmd)
        }
        guards::ToggleActionResult::None => None,
    }
}

pub(super) fn start_processing(state: &mut AppState) -> Option<TuiCommand> {
    if state.mode != AppMode::Select {
        return None;
    }

    match processing_logic::collect_start_processing_targets(&state.changes, state.parallel_mode) {
        Ok(selected) => {
            processing_logic::mark_changes_queued(&mut state.changes, &selected);
            processing_logic::sync_queue_intent(
                state.shared_orchestrator_state.as_ref(),
                &selected,
            );
            state.reset_for_run();
            state.mode = AppMode::Running;
            state.add_log(LogEntry::info(format!(
                "Starting processing {} change(s)",
                selected.len()
            )));
            Some(processing_logic::build_start_command(selected))
        }
        Err(message) => {
            state.warning_message = Some(message);
            None
        }
    }
}

pub(super) fn resume_processing(state: &mut AppState) -> Option<TuiCommand> {
    if state.mode != AppMode::Stopped {
        return None;
    }

    match processing_logic::collect_resume_processing_targets(&state.changes) {
        Ok(marked_ids) => {
            processing_logic::mark_changes_queued(&mut state.changes, &marked_ids);
            processing_logic::sync_queue_intent(
                state.shared_orchestrator_state.as_ref(),
                &marked_ids,
            );
            state.reset_for_run();
            state.mode = AppMode::Running;
            state.add_log(LogEntry::info(format!(
                "Resuming processing {} change(s)...",
                marked_ids.len()
            )));
            Some(processing_logic::build_start_command(marked_ids))
        }
        Err(message) => {
            state.warning_message = Some(message);
            None
        }
    }
}

pub(super) fn retry_error_changes(state: &mut AppState) -> Option<TuiCommand> {
    if state.mode != AppMode::Error {
        return None;
    }

    let error_ids = processing_logic::collect_retry_error_targets(&state.changes);
    if error_ids.is_empty() {
        return None;
    }

    let retry_ids = processing_logic::sync_retry_error_intent(
        state.shared_orchestrator_state.as_ref(),
        &error_ids,
    );
    if retry_ids.is_empty() {
        state.warning_message = Some("No marked error changes are retryable".to_string());
        return None;
    }

    processing_logic::mark_changes_queued(&mut state.changes, &retry_ids);
    for entry in processing_logic::emit_retry_logs(&retry_ids) {
        state.add_log(entry);
    }

    state.reset_for_run();
    state.mode = AppMode::Running;
    Some(processing_logic::build_start_command(retry_ids))
}

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

    fn make_change_state(
        id: &str,
        display_status_cache: &str,
        is_parallel_eligible: bool,
    ) -> ChangeState {
        ChangeState {
            id: id.to_string(),
            completed_tasks: 0,
            total_tasks: 1,
            display_status_cache: display_status_cache.to_string(),
            display_color_cache: Color::DarkGray,
            error_message_cache: None,
            selected: false,
            is_new: false,
            is_parallel_eligible,
            has_worktree: false,
            started_at: None,
            elapsed_time: None,
            iteration_number: None,
        }
    }

    #[test]
    fn running_mode_excludes_active_rows_from_bulk_toggle() {
        let change = make_change_state("active", "applying", true);

        assert!(!can_bulk_toggle_change(AppMode::Running, false, &change));
        assert!(can_bulk_toggle_change(AppMode::Select, false, &change));
    }

    #[test]
    fn parallel_mode_excludes_uncommitted_rows_from_bulk_toggle() {
        let ineligible = make_change_state("uncommitted", "not queued", false);

        assert!(!can_bulk_toggle_change(AppMode::Select, true, &ineligible));
        assert!(can_bulk_toggle_change(AppMode::Select, false, &ineligible));
    }
}