//! Estado global da TUI e reducers de transição (T-06, T-08, T-09, T-11, T-15,
//! T-18, T-19, T-20, T-22). Lógica de transição concentrada aqui; `ui.rs` apenas
//! desenha a partir deste estado.

use std::path::PathBuf;
use std::sync::mpsc::Receiver;
use std::time::{SystemTime, UNIX_EPOCH};

use crate::contract;
use crate::domain::providers::{
    provider_catalog_fast, resolve_provider_fast, ProviderCatalogEntry, ProviderResolveOptions,
    ProviderSelection,
};
use ratatui::text::Text;

use super::event::Action;
use super::execution::{
    self, CompletedTask, DiffReport, ExecutionTask, TaskRunStatus, WorkspaceSnapshot,
};
use super::loader::Loader;
use super::orchestration::{load_orchestration, scan_orchestrations, Orchestration};
use super::palette::{CommandId, CommandPalette};
use super::parallel::{self, AgentHandle, AgentSlotStatus, SlotResult};
use super::runner::{self, StageContextSample, StageEvent, StageResult, TokenUsageSample};
use super::stage::{can_navigate, Stage};
use super::{editor, persist};

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Screen {
    Welcome,
    ProviderSetup,
    OrchestrationList,
    NewOrchestration,
    StageBoard,
    Running,
    TaskExecution,
    TaskDecision,
    ParallelRunning,
    ParallelDecision,
    Viewer,
    Checkpoint,
    RefinementPrompt,
    ErrorPanel,
    Help,
    Summary,
}

/// Sobreposições flutuantes desenhadas por cima da tela ativa (estilo OpenCode).
/// São camada de apresentação/entrada: não alteram a máquina de estados de etapas.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Overlay {
    None,
    CommandPalette,
    SlashMenu,
    ProviderDialog,
    Telemetry,
    /// Modal de confirmação com botões focáveis (ex.: sair). O foco fica em `App::confirm_focus`.
    Confirm,
}

/// Traduz um comando do palette numa `Action` já tratada pelo reducer das telas.
/// Garante paridade palette↔teclas sem duplicar lógica de domínio.
fn command_to_action(command: CommandId) -> Action {
    match command {
        CommandId::NewOrchestration => Action::New,
        CommandId::OpenOrchestration | CommandId::OpenStage => Action::Confirm,
        CommandId::SwitchProvider => Action::Provider,
        CommandId::Approve => Action::Approve,
        CommandId::Regenerate => Action::Regenerate,
        CommandId::Edit => Action::Edit,
        CommandId::ToggleTelemetry => Action::Telemetry,
        CommandId::Help => Action::Help,
        CommandId::Quit => Action::Quit,
    }
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SlashCommandId {
    Stage(Stage),
    NewOrchestration,
    OpenOrchestrations,
    Provider,
    Model,
    Effort,
    ProviderSetup,
    Telemetry,
    Help,
    Editor,
    Exit,
}

pub struct SlashCommand {
    pub id: SlashCommandId,
    pub name: &'static str,
    pub description: &'static str,
}

pub const SLASH_COMMANDS: &[SlashCommand] = &[
    SlashCommand {
        id: SlashCommandId::Stage(Stage::Idea),
        name: "/idea",
        description: "Etapa 1 - descoberta da ideia",
    },
    SlashCommand {
        id: SlashCommandId::Stage(Stage::Prd),
        name: "/prd",
        description: "Etapa 2 - Product Requirements",
    },
    SlashCommand {
        id: SlashCommandId::Stage(Stage::Techspec),
        name: "/techspec",
        description: "Etapa 3 - arquitetura e contratos",
    },
    SlashCommand {
        id: SlashCommandId::Stage(Stage::Tasks),
        name: "/tasks",
        description: "Etapa 4 - backlog atômico",
    },
    SlashCommand {
        id: SlashCommandId::Stage(Stage::Refinement),
        name: "/refinement",
        description: "Etapa 5 - grooming opcional",
    },
    SlashCommand {
        id: SlashCommandId::Stage(Stage::Execution),
        name: "/execution",
        description: "Etapa 6 - execução por tasks",
    },
    SlashCommand {
        id: SlashCommandId::Stage(Stage::Adr),
        name: "/adr",
        description: "Etapa 6a - decisão arquitetural",
    },
    SlashCommand {
        id: SlashCommandId::Stage(Stage::Review),
        name: "/review",
        description: "Etapa 7 - review e QA",
    },
    SlashCommand {
        id: SlashCommandId::Stage(Stage::Memory),
        name: "/memory",
        description: "Etapa 8 - memória do fluxo",
    },
    SlashCommand {
        id: SlashCommandId::NewOrchestration,
        name: "/new",
        description: "Criar orquestração com o chat",
    },
    SlashCommand {
        id: SlashCommandId::OpenOrchestrations,
        name: "/open",
        description: "Abrir lista de orquestrações",
    },
    SlashCommand {
        id: SlashCommandId::Provider,
        name: "/provider",
        description: "Abrir dialog de provider",
    },
    SlashCommand {
        id: SlashCommandId::Model,
        name: "/model",
        description: "Trocar modelo no contexto atual",
    },
    SlashCommand {
        id: SlashCommandId::Effort,
        name: "/effort",
        description: "Trocar effort no contexto atual",
    },
    SlashCommand {
        id: SlashCommandId::ProviderSetup,
        name: "/connect",
        description: "Abrir dialog de provider",
    },
    SlashCommand {
        id: SlashCommandId::ProviderSetup,
        name: "/agents",
        description: "Trocar agente/provider",
    },
    SlashCommand {
        id: SlashCommandId::Editor,
        name: "/editor",
        description: "Abrir editor/config contextual",
    },
    SlashCommand {
        id: SlashCommandId::Telemetry,
        name: "/telemetry",
        description: "Abrir dashboard de tokens",
    },
    SlashCommand {
        id: SlashCommandId::Help,
        name: "/help",
        description: "Ajuda da TUI",
    },
    SlashCommand {
        id: SlashCommandId::Exit,
        name: "/exit",
        description: "Sair da aplicação",
    },
];

pub fn slash_visible_indices(query: &str) -> Vec<usize> {
    let query = query.trim().trim_start_matches('/').to_lowercase();
    let mut matched = SLASH_COMMANDS
        .iter()
        .enumerate()
        .filter_map(|(index, command)| {
            let name = command.name.trim_start_matches('/').to_lowercase();
            let label = format!("{name} {}", command.description.to_lowercase());
            slash_match_quality(&label, &query).map(|quality| (index, quality))
        })
        .collect::<Vec<_>>();
    matched.sort_by_key(|(index, quality)| (*quality, *index));
    matched.into_iter().map(|(index, _)| index).collect()
}

fn slash_match_quality(label: &str, query: &str) -> Option<u8> {
    if query.is_empty() {
        return Some(0);
    }
    if label.starts_with(query) {
        return Some(0);
    }
    if label.contains(query) {
        return Some(1);
    }
    let mut chars = label.chars();
    query
        .chars()
        .all(|needle| chars.any(|item| item == needle))
        .then_some(2)
}

/// Decisão registrada para telemetria (T-23).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CheckpointDecision {
    Approve,
    Regenerate,
    Edit,
    Skip,
}

impl CheckpointDecision {
    pub fn as_str(self) -> &'static str {
        match self {
            CheckpointDecision::Approve => "approve",
            CheckpointDecision::Regenerate => "regenerate",
            CheckpointDecision::Edit => "edit",
            CheckpointDecision::Skip => "skip",
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TaskDetailView {
    Log,
    Diff,
    Report,
}

impl TaskDetailView {
    pub fn next(self) -> Self {
        match self {
            TaskDetailView::Log => TaskDetailView::Diff,
            TaskDetailView::Diff => TaskDetailView::Report,
            TaskDetailView::Report => TaskDetailView::Log,
        }
    }

    pub fn previous(self) -> Self {
        match self {
            TaskDetailView::Log => TaskDetailView::Report,
            TaskDetailView::Diff => TaskDetailView::Log,
            TaskDetailView::Report => TaskDetailView::Diff,
        }
    }

    pub fn label(self) -> &'static str {
        match self {
            TaskDetailView::Log => "Log",
            TaskDetailView::Diff => "Diff",
            TaskDetailView::Report => "Report",
        }
    }
}

pub struct TaskRun {
    pub task: ExecutionTask,
    pub status: TaskRunStatus,
    pub trace: Vec<String>,
    pub usage: Option<TokenUsageSample>,
    pub result: Option<StageResult>,
    pub report_path: PathBuf,
    pub report: String,
    pub diff: Option<DiffReport>,
    pub(crate) before: Option<WorkspaceSnapshot>,
}

pub struct ExecutionSession {
    pub stage: Stage,
    pub force: bool,
    pub tasks: Vec<TaskRun>,
    pub selected_index: usize,
    pub detail_view: TaskDetailView,
    pub current_index: Option<usize>,
    pub rx: Option<Receiver<StageEvent>>,
}

/// Efeitos observáveis que o event loop (mod.rs) executa fora do reducer:
/// telemetria e disparo de subprocesso já são feitos aqui; este enum sinaliza
/// eventos que mod.rs deve logar.
#[derive(Clone, Debug, Default)]
pub struct SideEffects {
    pub log_stage_started: Option<Stage>,
    pub log_parallel_started: Option<(Stage, usize)>,
    pub log_parallel_finished: Option<(Stage, usize, usize)>,
    pub log_checkpoint: Option<(Stage, CheckpointDecision)>,
    pub log_orchestration_selected: Option<String>,
    pub log_provider_selected: Option<ProviderSelection>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SessionTokenUsage {
    pub input_tokens_estimate: usize,
    pub output_tokens_estimate: usize,
    pub total_tokens_estimate: usize,
    pub input_bytes: usize,
    pub output_bytes: usize,
    pub generation_count: usize,
    pub provider_reported_count: usize,
}

impl SessionTokenUsage {
    fn record(&mut self, sample: &TokenUsageSample) {
        self.input_tokens_estimate += sample.input_tokens_estimate;
        self.output_tokens_estimate += sample.output_tokens_estimate;
        self.total_tokens_estimate += sample.total_tokens_estimate;
        self.input_bytes += sample.input_bytes;
        self.output_bytes += sample.output_bytes;
        self.generation_count += 1;
        if sample.provider_reported {
            self.provider_reported_count += 1;
        }
    }
}

pub struct App {
    pub root: PathBuf,
    pub exe: PathBuf,
    pub screen: Screen,
    pub providers: Vec<ProviderCatalogEntry>,
    pub provider_index: usize,
    pub provider_model_index: usize,
    pub provider_effort_index: usize,
    pub provider_return: Screen,
    pub provider_selection: Option<ProviderSelection>,
    pub provider_error: Option<String>,
    pub orchestrations: Vec<Orchestration>,
    pub list_index: usize,
    pub active: Option<Orchestration>,
    pub board_index: usize,
    pub input: String,
    pub viewer_text: Option<Text<'static>>,
    pub viewer_raw: String,
    pub viewer_scroll: u16,
    pub viewer_stage: Option<Stage>,
    pub loader: Loader,
    pub error: Option<String>,
    pub error_stage: Option<Stage>,
    pub provider_trace: Vec<String>,
    pub session_usage: SessionTokenUsage,
    pub current_usage: Option<TokenUsageSample>,
    pub last_usage: Option<TokenUsageSample>,
    pub current_context: Option<StageContextSample>,
    pub last_context: Option<StageContextSample>,
    pub footer: Option<String>,
    pub help_return: Screen,
    pub pending: Option<Receiver<StageEvent>>,
    pub parallel: Vec<AgentHandle>,
    pub parallel_stage: Option<Stage>,
    pub execution: Option<ExecutionSession>,
    pub should_quit: bool,
    pub needs_redraw: bool,
    pub overlay: Overlay,
    pub palette: CommandPalette,
    pub slash_query: String,
    pub slash_index: usize,
    pub new_target_stage: Option<Stage>,
    /// Botão focado no modal de confirmação (0 = Cancelar, 1 = Sair).
    pub confirm_focus: usize,
}

impl App {
    pub fn new(root: PathBuf, exe: PathBuf) -> Self {
        let orchestrations = scan_orchestrations(&root);
        let (providers, provider_selection, provider_error) = load_provider_state(&root);
        let provider_index = provider_selection
            .as_ref()
            .and_then(|selection| providers.iter().position(|p| p.id == selection.id))
            .unwrap_or(0);
        let (provider_model_index, provider_effort_index) = provider_choice_indices(
            &providers,
            provider_index,
            provider_selection.as_ref(),
            None,
        );
        App {
            root,
            exe,
            screen: Screen::Welcome,
            providers,
            provider_index,
            provider_model_index,
            provider_effort_index,
            provider_return: Screen::OrchestrationList,
            provider_selection,
            provider_error,
            orchestrations,
            list_index: 0,
            active: None,
            board_index: 0,
            input: String::new(),
            viewer_text: None,
            viewer_raw: String::new(),
            viewer_scroll: 0,
            viewer_stage: None,
            loader: Loader::new(),
            error: None,
            error_stage: None,
            provider_trace: Vec::new(),
            session_usage: SessionTokenUsage::default(),
            current_usage: None,
            last_usage: None,
            current_context: None,
            last_context: None,
            footer: None,
            help_return: Screen::OrchestrationList,
            pending: None,
            parallel: Vec::new(),
            parallel_stage: None,
            execution: None,
            should_quit: false,
            needs_redraw: false,
            overlay: Overlay::None,
            palette: CommandPalette::default(),
            slash_query: String::new(),
            slash_index: 0,
            new_target_stage: None,
            confirm_focus: 0,
        }
    }

    pub fn text_mode(&self) -> bool {
        matches!(self.screen, Screen::NewOrchestration | Screen::Welcome)
            || matches!(self.overlay, Overlay::CommandPalette | Overlay::SlashMenu)
    }

    fn refresh_orchestrations(&mut self) {
        self.orchestrations = scan_orchestrations(&self.root);
    }

    fn reload_active(&mut self) {
        if let Some(active) = &self.active {
            if let Some(updated) = load_orchestration(&active.path.clone()) {
                self.active = Some(updated);
            }
        }
    }

    fn board_stage(&self) -> Stage {
        Stage::GUIDED[self.board_index.min(Stage::GUIDED.len() - 1)]
    }

    /// Etapas obrigatórias têm checkpoint humano; refinement também.
    fn stage_has_checkpoint(stage: Stage) -> bool {
        contract::stage_by_key(stage.key())
            .map(|spec| spec.requires_human_checkpoint)
            .unwrap_or(false)
    }

    /// Processa uma ação do usuário. Pode disparar efeitos colaterais (subprocesso,
    /// editor, persistência). Retorna SideEffects que mod.rs deve logar.
    pub fn update(&mut self, action: Action) -> SideEffects {
        let mut fx = SideEffects::default();
        self.footer = None;
        // Overlays têm prioridade de captura sobre a tela ativa.
        if self.overlay != Overlay::None {
            self.update_overlay(action, &mut fx);
            return fx;
        }
        // Abertura de overlay a partir de qualquer tela.
        match action {
            Action::OpenPalette => self.open_palette(),
            Action::OpenSlashMenu => self.open_slash_menu(),
            Action::Telemetry => self.overlay = Overlay::Telemetry,
            Action::Provider => self.open_provider_dialog(),
            Action::NextProvider | Action::PreviousProvider
                if !matches!(
                    self.screen,
                    Screen::ProviderSetup | Screen::TaskExecution | Screen::TaskDecision
                ) =>
            {
                self.open_provider_dialog();
            }
            // Sair pede confirmação num modal com botões focáveis (estilo OpenCode).
            Action::Quit => {
                self.confirm_focus = 1;
                self.overlay = Overlay::Confirm;
            }
            other => self.dispatch_screen(other, &mut fx),
        }
        fx
    }

    /// Despacho por tela (sem overlays). Reusado pelo palette ao resolver um comando.
    fn dispatch_screen(&mut self, action: Action, fx: &mut SideEffects) {
        match self.screen {
            Screen::Welcome => self.update_welcome(action, fx),
            Screen::ProviderSetup => self.update_provider_setup(action, fx),
            Screen::OrchestrationList => self.update_list(action, fx),
            Screen::NewOrchestration => self.update_new(action, fx),
            Screen::StageBoard => self.update_board(action, fx),
            Screen::Running => self.update_running(action),
            Screen::TaskExecution => self.update_task_execution(action),
            Screen::TaskDecision => self.update_task_decision(action, fx),
            Screen::ParallelRunning => self.update_parallel_running(action),
            Screen::ParallelDecision => self.update_parallel_decision(action, fx),
            Screen::Viewer => self.update_viewer(action, fx),
            Screen::Checkpoint => self.update_checkpoint(action, fx),
            Screen::RefinementPrompt => self.update_refinement_prompt(action, fx),
            Screen::ErrorPanel => self.update_error(action, fx),
            Screen::Help => self.update_help(action),
            Screen::Summary => self.update_summary(action),
        }
    }

    fn open_palette(&mut self) {
        self.palette.open();
        self.overlay = Overlay::CommandPalette;
    }

    fn open_slash_menu(&mut self) {
        self.slash_query.clear();
        self.slash_index = 0;
        self.overlay = Overlay::SlashMenu;
    }

    fn open_provider_dialog(&mut self) {
        self.sync_provider_choice_indices_for_stage(self.current_provider_stage());
        self.overlay = Overlay::ProviderDialog;
    }

    /// Trata teclas enquanto um overlay está aberto. Um comando do palette resolve
    /// para uma `Action` já existente e é re-despachado pela tela ativa (RN-01).
    fn update_overlay(&mut self, action: Action, fx: &mut SideEffects) {
        match self.overlay {
            Overlay::CommandPalette => match action {
                Action::Back => {
                    self.palette.close();
                    self.overlay = Overlay::None;
                }
                Action::Up => self.palette.move_up(),
                Action::Down => self.palette.move_down(),
                Action::Char(c) => self.palette.push_char(c),
                Action::Backspace => self.palette.backspace(),
                Action::Confirm => {
                    let command = self.palette.selected_command();
                    self.palette.close();
                    self.overlay = Overlay::None;
                    if let Some(command) = command {
                        match command_to_action(command) {
                            Action::OpenPalette => self.open_palette(),
                            Action::Telemetry => self.overlay = Overlay::Telemetry,
                            Action::Provider => self.open_provider_dialog(),
                            resolved => self.dispatch_screen(resolved, fx),
                        }
                    }
                }
                _ => {}
            },
            Overlay::SlashMenu => match action {
                Action::Back => self.close_slash_menu(),
                Action::Up => self.move_slash_selection(-1),
                Action::Down => self.move_slash_selection(1),
                Action::Char(c) => {
                    self.slash_query.push(c);
                    self.clamp_slash_selection();
                }
                Action::Backspace => {
                    if self.slash_query.is_empty() {
                        self.close_slash_menu();
                    } else {
                        self.slash_query.pop();
                        self.clamp_slash_selection();
                    }
                }
                Action::NextProvider | Action::PreviousProvider | Action::Provider => {
                    self.close_slash_menu();
                    self.open_provider_dialog();
                }
                Action::NextModel => self.quick_cycle_model(1, fx),
                Action::PreviousModel => self.quick_cycle_model(-1, fx),
                Action::NextEffort => self.quick_cycle_effort(1, fx),
                Action::PreviousEffort => self.quick_cycle_effort(-1, fx),
                Action::Confirm => {
                    let command = self.selected_slash_command();
                    self.close_slash_menu();
                    if let Some(command) = command {
                        self.execute_slash_command(command, fx);
                    }
                }
                _ => {}
            },
            Overlay::ProviderDialog => match action {
                Action::Back | Action::Provider => self.overlay = Overlay::None,
                Action::Up | Action::PreviousProvider => {
                    if self.provider_index > 0 {
                        self.provider_index -= 1;
                    } else if !self.providers.is_empty() {
                        self.provider_index = self.providers.len() - 1;
                    }
                    self.sync_provider_choice_indices_for_stage(self.current_provider_stage());
                }
                Action::Down | Action::NextProvider => {
                    if !self.providers.is_empty() {
                        self.provider_index =
                            cycle_index(self.provider_index, self.providers.len(), 1);
                    }
                    self.sync_provider_choice_indices_for_stage(self.current_provider_stage());
                }
                Action::PreviousModel => self.cycle_provider_model(-1),
                Action::NextModel => self.cycle_provider_model(1),
                Action::PreviousEffort => self.cycle_provider_effort(-1),
                Action::NextEffort => self.cycle_provider_effort(1),
                Action::Regenerate => {
                    let (providers, selection, error) = load_provider_state(&self.root);
                    self.providers = providers;
                    self.provider_selection = selection;
                    self.provider_error = error;
                    self.sync_provider_choice_indices_for_stage(self.current_provider_stage());
                }
                Action::Edit => {
                    self.overlay = Overlay::None;
                    self.open_context_editor();
                }
                Action::Confirm
                    if self.apply_provider_choice(self.current_provider_stage(), fx) =>
                {
                    self.overlay = Overlay::None;
                }
                _ => {}
            },
            Overlay::Telemetry => {
                if matches!(action, Action::Back | Action::Telemetry | Action::Confirm) {
                    self.overlay = Overlay::None;
                }
            }
            Overlay::Confirm => match action {
                Action::Back => self.overlay = Overlay::None,
                Action::Confirm => {
                    if self.confirm_focus == 1 {
                        self.should_quit = true;
                    }
                    self.overlay = Overlay::None;
                }
                Action::PreviousModel
                | Action::NextModel
                | Action::NextProvider
                | Action::PreviousProvider
                | Action::NextEffort
                | Action::PreviousEffort
                | Action::Up
                | Action::Down => {
                    self.confirm_focus = 1 - self.confirm_focus.min(1);
                }
                _ => {}
            },
            Overlay::None => {}
        }
    }

    fn close_slash_menu(&mut self) {
        self.overlay = Overlay::None;
        self.slash_query.clear();
        self.slash_index = 0;
    }

    fn move_slash_selection(&mut self, delta: isize) {
        let visible = slash_visible_indices(&self.slash_query);
        if visible.is_empty() {
            self.slash_index = 0;
            return;
        }
        let len = visible.len() as isize;
        self.slash_index =
            (self.slash_index as isize + delta).clamp(0, len.saturating_sub(1)) as usize;
    }

    fn clamp_slash_selection(&mut self) {
        let len = slash_visible_indices(&self.slash_query).len();
        if len == 0 {
            self.slash_index = 0;
        } else if self.slash_index >= len {
            self.slash_index = len - 1;
        }
    }

    fn selected_slash_command(&self) -> Option<SlashCommandId> {
        let visible = slash_visible_indices(&self.slash_query);
        visible
            .get(self.slash_index)
            .and_then(|index| SLASH_COMMANDS.get(*index))
            .map(|command| command.id)
    }

    fn execute_slash_command(&mut self, command: SlashCommandId, fx: &mut SideEffects) {
        match command {
            SlashCommandId::Stage(stage) => self.jump_to_stage(stage, fx),
            SlashCommandId::NewOrchestration => {
                self.input.clear();
                self.new_target_stage = None;
                self.screen = Screen::NewOrchestration;
            }
            SlashCommandId::OpenOrchestrations => {
                self.refresh_orchestrations();
                self.screen = Screen::OrchestrationList;
            }
            SlashCommandId::Provider => self.open_provider_dialog(),
            SlashCommandId::Model => self.quick_cycle_model(1, fx),
            SlashCommandId::Effort => self.quick_cycle_effort(1, fx),
            SlashCommandId::ProviderSetup => self.open_provider_dialog(),
            SlashCommandId::Telemetry => self.overlay = Overlay::Telemetry,
            SlashCommandId::Help => self.open_help(),
            SlashCommandId::Editor => self.open_context_editor(),
            SlashCommandId::Exit => {
                self.confirm_focus = 1;
                self.overlay = Overlay::Confirm;
            }
        }
    }

    fn jump_to_stage(&mut self, stage: Stage, fx: &mut SideEffects) {
        let Some(active) = self.active.as_ref() else {
            self.input.clear();
            self.new_target_stage = Some(stage);
            self.screen = Screen::NewOrchestration;
            self.footer = Some(format!("Descreva o contexto para gerar {}", stage.label()));
            return;
        };
        if let Some(index) = Stage::GUIDED.iter().position(|item| *item == stage) {
            self.board_index = index;
        }
        let stages = active.stages.clone();
        if let Err(msg) = can_navigate(&stages, stage) {
            self.footer = Some(msg);
            self.screen = Screen::StageBoard;
            return;
        }
        if !Stage::GUIDED.contains(&stage) {
            self.viewer_stage = Some(stage);
            if (self.stage_file_exists(stage) || stage == Stage::Adr) && self.load_viewer(stage) {
                self.screen = Screen::Viewer;
            } else {
                self.start_stage(stage, false, fx);
            }
            return;
        }
        self.screen = Screen::StageBoard;
        self.dispatch_screen(Action::Confirm, fx);
    }

    fn open_context_editor(&mut self) {
        if let Some(stage) = self.viewer_stage {
            if matches!(self.screen, Screen::Viewer | Screen::Checkpoint) {
                self.edit_stage(stage);
                return;
            }
        }
        let cfg = self.root.join("sdd.config.yaml");
        if let Err(e) = editor::open_editor(&cfg) {
            self.error = Some(e.to_string());
            self.error_stage = None;
            self.screen = Screen::ErrorPanel;
            return;
        }
        let (providers, selection, error) = load_provider_state(&self.root);
        self.providers = providers;
        self.provider_selection = selection;
        self.provider_error = error;
        self.sync_provider_choice_indices();
        self.needs_redraw = true;
    }

    fn quick_cycle_provider(&mut self, delta: isize, fx: &mut SideEffects) {
        if self.providers.is_empty() {
            self.footer = Some("Nenhum provider disponível; edite sdd.config.yaml".to_string());
            return;
        }
        self.provider_index = cycle_index(self.provider_index, self.providers.len(), delta);
        self.sync_provider_choice_indices_for_stage(self.current_provider_stage());
        self.apply_provider_choice(self.current_provider_stage(), fx);
    }

    fn quick_cycle_model(&mut self, delta: isize, fx: &mut SideEffects) {
        let stage = self.current_provider_stage();
        self.sync_provider_choice_indices_for_stage(stage);
        self.cycle_provider_model(delta);
        self.apply_provider_choice(stage, fx);
    }

    fn quick_cycle_effort(&mut self, delta: isize, fx: &mut SideEffects) {
        let stage = self.current_provider_stage();
        self.sync_provider_choice_indices_for_stage(stage);
        self.cycle_provider_effort(delta);
        self.apply_provider_choice(stage, fx);
    }

    fn apply_provider_choice(
        &mut self,
        stage_override: Option<Stage>,
        fx: &mut SideEffects,
    ) -> bool {
        if self.providers.is_empty() {
            self.footer = Some("Nenhum provider disponível; edite sdd.config.yaml".to_string());
            return false;
        }
        let provider = self.providers[self.provider_index.min(self.providers.len() - 1)].clone();
        if !provider.enabled {
            self.footer = Some(format!(
                "Provider {} está desabilitado em sdd.config.yaml",
                provider.id
            ));
            return false;
        }
        let model = selected_provider_model(&provider, self.provider_model_index);
        let effort = selected_provider_effort(&provider, self.provider_effort_index);
        let options = ProviderResolveOptions {
            provider: Some(provider.id.clone()),
            model: Some(model.clone()),
            effort: Some(effort.clone()),
            offline: false,
        };
        match resolve_provider_fast(&self.root, &options) {
            Ok(mut selection) => {
                if let Some(stage) = stage_override {
                    selection
                        .stage_models
                        .insert(stage.key().to_string(), model.clone());
                    selection
                        .stage_efforts
                        .insert(stage.key().to_string(), effort.clone());
                }
                fx.log_provider_selected = Some(selection.clone());
                self.provider_selection = Some(selection);
                let scope = stage_override
                    .map(|stage| stage.label().to_string())
                    .unwrap_or_else(|| "sessão".to_string());
                self.footer = Some(format!(
                    "Rota {scope}: {} · {} · {}",
                    provider.id, model, effort
                ));
                true
            }
            Err(e) => {
                self.provider_error = Some(e.to_string());
                self.footer = Some("Não foi possível resolver o provider".to_string());
                false
            }
        }
    }

    fn current_provider_stage(&self) -> Option<Stage> {
        match self.screen {
            Screen::StageBoard => Some(self.board_stage()),
            Screen::Running | Screen::Viewer | Screen::Checkpoint => self.viewer_stage,
            Screen::RefinementPrompt => Some(Stage::Refinement),
            Screen::TaskExecution | Screen::TaskDecision => Some(Stage::Execution),
            Screen::ParallelRunning | Screen::ParallelDecision => self.parallel_stage,
            _ => None,
        }
    }

    fn provider_return_stage_context(&self) -> Option<Stage> {
        match self.provider_return {
            Screen::StageBoard => Some(self.board_stage()),
            Screen::Running | Screen::Viewer | Screen::Checkpoint => self.viewer_stage,
            Screen::RefinementPrompt => Some(Stage::Refinement),
            Screen::TaskExecution | Screen::TaskDecision => Some(Stage::Execution),
            Screen::ParallelRunning | Screen::ParallelDecision => self.parallel_stage,
            _ => None,
        }
    }

    fn open_help(&mut self) {
        self.help_return = self.screen;
        self.screen = Screen::Help;
    }

    fn update_provider_setup(&mut self, action: Action, fx: &mut SideEffects) {
        match action {
            Action::Quit => self.should_quit = true,
            Action::Help => self.open_help(),
            Action::PreviousProvider => {
                if !self.providers.is_empty() {
                    self.provider_index =
                        cycle_index(self.provider_index, self.providers.len(), -1);
                    self.sync_provider_choice_indices();
                }
            }
            Action::NextProvider => {
                if !self.providers.is_empty() {
                    self.provider_index = cycle_index(self.provider_index, self.providers.len(), 1);
                    self.sync_provider_choice_indices();
                }
            }
            Action::Up => {
                if self.provider_index > 0 {
                    self.provider_index -= 1;
                    self.sync_provider_choice_indices();
                }
            }
            Action::Down => {
                if self.provider_index + 1 < self.providers.len() {
                    self.provider_index += 1;
                    self.sync_provider_choice_indices();
                }
            }
            Action::PreviousModel => self.cycle_provider_model(-1),
            Action::NextModel => self.cycle_provider_model(1),
            Action::PreviousEffort => self.cycle_provider_effort(-1),
            Action::NextEffort => self.cycle_provider_effort(1),
            Action::Regenerate => {
                let (providers, selection, error) = load_provider_state(&self.root);
                self.providers = providers;
                self.provider_selection = selection;
                self.provider_error = error;
                if let Some(selection) = &self.provider_selection {
                    self.provider_index = self
                        .providers
                        .iter()
                        .position(|p| p.id == selection.id)
                        .unwrap_or(0);
                }
                self.sync_provider_choice_indices();
            }
            Action::Edit => {
                let cfg = self.root.join("sdd.config.yaml");
                if let Err(e) = editor::open_editor(&cfg) {
                    self.error = Some(e.to_string());
                    self.error_stage = None;
                    self.screen = Screen::ErrorPanel;
                    return;
                }
                let (providers, selection, error) = load_provider_state(&self.root);
                self.providers = providers;
                self.provider_selection = selection;
                self.provider_error = error;
                self.sync_provider_choice_indices();
                self.needs_redraw = true;
            }
            Action::Back => {
                if self.provider_selection.is_some() {
                    self.screen = self.provider_return;
                } else {
                    self.footer = Some("Escolha um provider antes de avançar".to_string());
                }
            }
            Action::Confirm => {
                let stage = self.provider_return_stage_context();
                if self.apply_provider_choice(stage, fx) {
                    self.screen = self.provider_return;
                }
            }
            _ => {}
        }
    }

    fn sync_provider_choice_indices(&mut self) {
        self.sync_provider_choice_indices_for_stage(None);
    }

    fn sync_provider_choice_indices_for_stage(&mut self, stage: Option<Stage>) {
        let (model_index, effort_index) = provider_choice_indices(
            &self.providers,
            self.provider_index,
            self.provider_selection.as_ref(),
            stage,
        );
        self.provider_model_index = model_index;
        self.provider_effort_index = effort_index;
    }

    fn cycle_provider_model(&mut self, delta: isize) {
        let Some(provider) = self.providers.get(self.provider_index) else {
            return;
        };
        let len = provider.models.len().max(1);
        self.provider_model_index = cycle_index(self.provider_model_index, len, delta);
    }

    fn cycle_provider_effort(&mut self, delta: isize) {
        let Some(provider) = self.providers.get(self.provider_index) else {
            return;
        };
        let len = provider.efforts.len().max(1);
        self.provider_effort_index = cycle_index(self.provider_effort_index, len, delta);
    }

    fn update_list(&mut self, action: Action, fx: &mut SideEffects) {
        match action {
            Action::Quit => self.should_quit = true,
            Action::Help => self.open_help(),
            Action::Up => {
                if self.list_index > 0 {
                    self.list_index -= 1;
                }
            }
            Action::Down => {
                if self.list_index + 1 < self.orchestrations.len() {
                    self.list_index += 1;
                }
            }
            Action::New => {
                self.input.clear();
                self.new_target_stage = None;
                self.screen = Screen::NewOrchestration;
            }
            Action::Provider => {
                self.provider_return = self.screen;
                self.screen = Screen::ProviderSetup;
            }
            Action::NextProvider => self.quick_cycle_provider(1, fx),
            Action::PreviousProvider => self.quick_cycle_provider(-1, fx),
            Action::NextModel => self.quick_cycle_model(1, fx),
            Action::PreviousModel => self.quick_cycle_model(-1, fx),
            Action::NextEffort => self.quick_cycle_effort(1, fx),
            Action::PreviousEffort => self.quick_cycle_effort(-1, fx),
            Action::Confirm => {
                if let Some(o) = self.orchestrations.get(self.list_index).cloned() {
                    fx.log_orchestration_selected = Some(o.slug.clone());
                    self.board_index = o
                        .current_stage()
                        .map(|s| Stage::GUIDED.iter().position(|x| *x == s).unwrap_or(0))
                        .unwrap_or(0);
                    self.active = Some(o);
                    self.screen = Screen::StageBoard;
                }
            }
            _ => {}
        }
    }

    /// Tela inicial (home estilo OpenCode): input vivo + atalhos.
    /// Enter com texto cria a orquestração; Enter vazio abre a lista; Esc vai à lista.
    fn update_welcome(&mut self, action: Action, fx: &mut SideEffects) {
        match action {
            Action::Confirm if self.input.trim().is_empty() => {
                self.screen = Screen::OrchestrationList;
            }
            Action::Back => {
                self.new_target_stage = None;
                self.screen = Screen::OrchestrationList;
            }
            Action::Provider => {
                self.provider_return = self.screen;
                self.screen = Screen::ProviderSetup;
            }
            Action::NextProvider => self.quick_cycle_provider(1, fx),
            Action::PreviousProvider => self.quick_cycle_provider(-1, fx),
            Action::NextModel => self.quick_cycle_model(1, fx),
            Action::PreviousModel => self.quick_cycle_model(-1, fx),
            Action::NextEffort => self.quick_cycle_effort(1, fx),
            Action::PreviousEffort => self.quick_cycle_effort(-1, fx),
            Action::Help => self.open_help(),
            other => self.update_new(other, fx),
        }
    }

    fn update_new(&mut self, action: Action, fx: &mut SideEffects) {
        match action {
            Action::Back => {
                self.screen = Screen::OrchestrationList;
            }
            Action::Char(c) => self.input.push(c),
            Action::Backspace => {
                self.input.pop();
            }
            Action::NextProvider => self.quick_cycle_provider(1, fx),
            Action::PreviousProvider => self.quick_cycle_provider(-1, fx),
            Action::NextModel => self.quick_cycle_model(1, fx),
            Action::PreviousModel => self.quick_cycle_model(-1, fx),
            Action::NextEffort => self.quick_cycle_effort(1, fx),
            Action::PreviousEffort => self.quick_cycle_effort(-1, fx),
            Action::Confirm => {
                let idea = self.input.trim().to_string();
                if idea.is_empty() {
                    self.footer = Some("Digite uma ideia antes de confirmar".to_string());
                    return;
                }
                match runner::run_init(&self.exe, &self.root, &idea) {
                    Ok(out) if out.status.success() => {
                        self.refresh_orchestrations();
                        // Seleciona a recém-criada pelo slug derivado.
                        if let Some(idx) = self
                            .orchestrations
                            .iter()
                            .position(|o| o.name == idea || o.slug == slugify_like(&idea))
                        {
                            self.list_index = idx;
                            self.active = Some(self.orchestrations[idx].clone());
                            fx.log_orchestration_selected =
                                Some(self.orchestrations[idx].slug.clone());
                            let target = self.new_target_stage.take();
                            if let Some(stage) = target {
                                if let Some(index) =
                                    Stage::GUIDED.iter().position(|item| *item == stage)
                                {
                                    self.board_index = index;
                                }
                                self.start_target_stage_from_new(stage, fx);
                            } else {
                                self.board_index = 0;
                                self.screen = Screen::StageBoard;
                            }
                        } else {
                            self.new_target_stage = None;
                            self.screen = Screen::OrchestrationList;
                        }
                    }
                    Ok(out) => {
                        self.error = Some(String::from_utf8_lossy(&out.stderr).into_owned());
                        self.error_stage = None;
                        self.screen = Screen::ErrorPanel;
                    }
                    Err(e) => {
                        self.error = Some(format!("falha ao executar sdd init: {e}"));
                        self.error_stage = None;
                        self.screen = Screen::ErrorPanel;
                    }
                }
            }
            _ => {}
        }
    }

    fn start_target_stage_from_new(&mut self, stage: Stage, fx: &mut SideEffects) {
        self.screen = Screen::StageBoard;
        self.viewer_stage = Some(stage);
        match stage {
            Stage::Execution => self.start_execution_stage(false, fx),
            _ => self.start_stage(stage, false, fx),
        }
    }

    fn update_board(&mut self, action: Action, fx: &mut SideEffects) {
        match action {
            Action::Back => {
                self.active = None;
                self.screen = Screen::OrchestrationList;
            }
            Action::Quit => self.should_quit = true,
            Action::Help => self.open_help(),
            Action::Provider => {
                self.provider_return = self.screen;
                self.screen = Screen::ProviderSetup;
            }
            Action::NextProvider => self.quick_cycle_provider(1, fx),
            Action::PreviousProvider => self.quick_cycle_provider(-1, fx),
            Action::NextModel => self.quick_cycle_model(1, fx),
            Action::PreviousModel => self.quick_cycle_model(-1, fx),
            Action::NextEffort => self.quick_cycle_effort(1, fx),
            Action::PreviousEffort => self.quick_cycle_effort(-1, fx),
            Action::Up => {
                if self.board_index > 0 {
                    self.board_index -= 1;
                }
            }
            Action::Down => {
                if self.board_index + 1 < Stage::GUIDED.len() {
                    self.board_index += 1;
                }
            }
            Action::Confirm => {
                let target = self.board_stage();
                let stages = match &self.active {
                    Some(a) => a.stages.clone(),
                    None => return,
                };
                if let Err(msg) = can_navigate(&stages, target) {
                    self.footer = Some(msg);
                    return;
                }
                // Etapa já cumprida com arquivo presente → modo leitura.
                let already = stages
                    .iter()
                    .find(|s| s.stage == target)
                    .map(|s| s.status.is_cleared())
                    .unwrap_or(false);
                if (already || self.stage_file_exists(target)) && self.load_viewer(target) {
                    self.viewer_stage = Some(target);
                    self.screen = Screen::Viewer;
                    return;
                }
                if target == Stage::Refinement {
                    self.screen = Screen::RefinementPrompt;
                    return;
                }
                if target == Stage::Execution {
                    self.start_execution_stage(false, fx);
                    return;
                }
                if parallel::supports_parallel(target) {
                    self.start_parallel_stage(target, false, fx);
                    return;
                }
                self.start_stage(target, false, fx);
            }
            _ => {}
        }
    }

    fn start_stage(&mut self, stage: Stage, force: bool, fx: &mut SideEffects) {
        let name = match &self.active {
            Some(a) => a.name.clone(),
            None => return,
        };
        self.parallel.clear();
        self.parallel_stage = None;
        self.execution = None;
        let provider_label = self
            .provider_selection
            .as_ref()
            .map(|p| {
                let model = self.model_for_stage(p, stage);
                let effort = self.effort_for_stage(p, stage);
                format!("{} · {} · {}", p.id, model, effort)
            })
            .unwrap_or_else(|| "provider padrão".to_string());
        let verb = if force { "Regenerando" } else { "Executando" };
        self.loader
            .arm(format!("{verb} {} via {provider_label}…", stage.label()));
        self.provider_trace = self.stage_trace_preview(stage, force);
        self.current_usage = None;
        self.current_context = None;
        self.pending = Some(runner::spawn_stage(
            self.exe.clone(),
            stage,
            name,
            self.root.clone(),
            None,
            self.stage_run_options(force, stage),
        ));
        self.viewer_stage = Some(stage);
        self.screen = Screen::Running;
        fx.log_stage_started = Some(stage);
    }

    fn start_execution_stage(&mut self, force: bool, fx: &mut SideEffects) {
        let active = match &self.active {
            Some(active) => active.clone(),
            None => return,
        };
        let tasks_path = active.path.join(Stage::Tasks.filename());
        let tasks_md = match std::fs::read_to_string(&tasks_path) {
            Ok(content) => content,
            Err(e) => {
                self.error = Some(format!(
                    "Não foi possível ler {} para executar tasks reais: {e}",
                    tasks_path.display()
                ));
                self.error_stage = Some(Stage::Execution);
                self.screen = Screen::ErrorPanel;
                return;
            }
        };
        let tasks = execution::parse_execution_tasks(&tasks_md);
        if tasks.is_empty() {
            self.error = Some(format!(
                "{} não contém tasks reconhecidas. Use tabela `T-01` em `## Backlog` e detalhes `### T-01 — ...` em `## Prompts Agent`, ou itens `### TSK-01 — ...`.",
                Stage::Tasks.filename()
            ));
            self.error_stage = Some(Stage::Execution);
            self.screen = Screen::ErrorPanel;
            return;
        }

        let provider_id = self
            .provider_selection
            .as_ref()
            .map(|provider| provider.id.as_str())
            .unwrap_or("auto");
        if provider_id != "codex" {
            self.error = Some(format!(
                "Execução real de tasks com edição de workspace ainda exige provider `codex`; `{provider_id}` já pode gerar artefatos via adapter direto quando autenticado, mas não executa patches reais nesta versão."
            ));
            self.error_stage = Some(Stage::Execution);
            self.screen = Screen::ErrorPanel;
            return;
        }

        self.pending = None;
        self.parallel.clear();
        self.parallel_stage = None;
        self.current_usage = None;
        self.current_context = None;
        self.provider_trace = vec![
            format!("execution_started: {} task(s)", tasks.len()),
            "modo: task-by-task com workspace-write".to_string(),
            "diff: snapshot por task; git no resumo final quando disponível".to_string(),
        ];

        let runs = tasks
            .into_iter()
            .map(|task| {
                let report_path = execution_report_path(&active.path, &task.id);
                TaskRun {
                    task,
                    status: TaskRunStatus::Pending,
                    trace: Vec::new(),
                    usage: None,
                    result: None,
                    report_path,
                    report: String::new(),
                    diff: None,
                    before: None,
                }
            })
            .collect::<Vec<_>>();

        self.execution = Some(ExecutionSession {
            stage: Stage::Execution,
            force,
            tasks: runs,
            selected_index: 0,
            detail_view: TaskDetailView::Log,
            current_index: None,
            rx: None,
        });
        self.viewer_stage = Some(Stage::Execution);
        self.screen = Screen::TaskExecution;
        fx.log_stage_started = Some(Stage::Execution);
        self.start_next_execution_task();
    }

    fn start_next_execution_task(&mut self) {
        let Some(index) = self.execution.as_ref().and_then(|session| {
            session
                .tasks
                .iter()
                .position(|task| task.status == TaskRunStatus::Pending)
        }) else {
            return;
        };
        self.start_execution_task_at(index);
    }

    fn start_execution_task_at(&mut self, index: usize) {
        let (stage, force, task, report_path, name, active_path) = {
            let Some(session) = self.execution.as_ref() else {
                return;
            };
            let Some(active) = self.active.as_ref() else {
                return;
            };
            let Some(run) = session.tasks.get(index) else {
                return;
            };
            (
                session.stage,
                session.force,
                run.task.clone(),
                run.report_path.clone(),
                active.name.clone(),
                active.path.clone(),
            )
        };

        let before = match execution::capture_workspace_snapshot(&self.root, Some(&active_path)) {
            Ok(snapshot) => snapshot,
            Err(e) => {
                if let Some(session) = self.execution.as_mut() {
                    if let Some(run) = session.tasks.get_mut(index) {
                        run.status = TaskRunStatus::Failed;
                        run.trace
                            .push(format!("falha ao capturar snapshot inicial: {e}"));
                    }
                    session.selected_index = index;
                }
                self.loader.disarm();
                self.footer = Some("Falha antes de iniciar a task; retente ou cancele".to_string());
                self.screen = Screen::TaskDecision;
                return;
            }
        };
        let options = self.stage_run_options(force, stage);
        let rx = runner::spawn_execution_task(
            stage,
            name,
            task.clone(),
            self.root.clone(),
            report_path,
            options,
        );

        if let Some(session) = self.execution.as_mut() {
            if let Some(run) = session.tasks.get_mut(index) {
                run.status = TaskRunStatus::Running;
                run.trace = vec![format!("task {} iniciada", task.id)];
                run.usage = None;
                run.result = None;
                run.report.clear();
                run.diff = None;
                run.before = Some(before);
            }
            session.current_index = Some(index);
            session.selected_index = index;
            session.rx = Some(rx);
        }
        self.loader
            .arm(format!("Executando {} — {}…", task.id, task.title));
        self.screen = Screen::TaskExecution;
    }

    fn start_parallel_stage(&mut self, stage: Stage, force: bool, fx: &mut SideEffects) {
        let active = match &self.active {
            Some(active) => active.clone(),
            None => return,
        };
        let tasks_path = active.path.join(Stage::Tasks.filename());
        let tasks_md = match std::fs::read_to_string(&tasks_path) {
            Ok(content) => content,
            Err(_) => {
                self.footer = Some(format!(
                    "Sem {}, usando execução sequencial",
                    Stage::Tasks.filename()
                ));
                self.start_stage(stage, force, fx);
                return;
            }
        };
        let tasks = parallel::plan_parallel_slots(&tasks_md)
            .into_iter()
            .filter(|task| task.independent)
            .collect::<Vec<_>>();
        if tasks.is_empty() {
            self.footer = Some("Nenhuma task independente; usando execução sequencial".to_string());
            self.start_stage(stage, force, fx);
            return;
        }

        let provider_label = self
            .provider_selection
            .as_ref()
            .map(|p| {
                let model = self.model_for_stage(p, stage);
                let effort = self.effort_for_stage(p, stage);
                format!("{} · {} · {}", p.id, model, effort)
            })
            .unwrap_or_else(|| "provider padrão".to_string());
        self.loader.arm(format!(
            "Executando {} AgentSlots de {} via {provider_label}…",
            tasks.len(),
            stage.label()
        ));
        self.provider_trace = vec![
            format!("parallel_started: {} slot(s)", tasks.len()),
            format!("stage: {}", stage.key()),
        ];
        self.current_usage = None;
        self.current_context = None;
        self.pending = None;
        self.execution = None;
        self.parallel.clear();

        let options = self.stage_run_options(force, stage);
        for task in tasks {
            let fragment_path = parallel_fragment_path(&active.path, stage, &task.id);
            let rx = runner::spawn_parallel_agent(
                self.exe.clone(),
                stage,
                active.name.clone(),
                task.clone(),
                self.root.clone(),
                fragment_path.clone(),
                options.clone(),
            );
            self.parallel.push(AgentHandle {
                task_id: task.id.clone(),
                task_label: task.title.clone(),
                rx,
                status: AgentSlotStatus::Aguardando,
                trace: vec![format!("slot criado para {}", task.id)],
                usage: None,
                result: None,
                fragment_path,
            });
        }
        self.parallel_stage = Some(stage);
        self.viewer_stage = Some(stage);
        self.screen = Screen::ParallelRunning;
        fx.log_stage_started = Some(stage);
        fx.log_parallel_started = Some((stage, self.parallel.len()));
    }

    fn stage_run_options(&self, force: bool, stage: Stage) -> runner::StageRunOptions {
        let mut options = runner::StageRunOptions {
            force,
            ..runner::StageRunOptions::default()
        };
        if let Some(provider) = &self.provider_selection {
            options.provider = Some(provider.id.clone());
            let model = self.model_for_stage(provider, stage);
            if let Some(model_definition) = provider.models.iter().find(|item| item.id == model) {
                options.context_window_tokens = model_definition.context_window_tokens;
                options.pricing = Some(model_definition.pricing.clone());
            }
            options.model = Some(model);
            options.effort = Some(self.effort_for_stage(provider, stage));
            options.offline = provider.offline;
        }
        options.artifact_dir = self.active.as_ref().map(|active| active.path.clone());
        options
    }

    fn stage_trace_preview(&self, stage: Stage, force: bool) -> Vec<String> {
        let mut trace = Vec::new();
        trace.push(format!(
            "acao: {}",
            if force {
                "regenerar artefato"
            } else {
                "gerar artefato"
            }
        ));
        if let Some(provider) = &self.provider_selection {
            let context_model = self.model_for_stage(provider, stage);
            let context_effort = self.effort_for_stage(provider, stage);
            trace.push(format!(
                "rota resolvida: {} · {context_model} · {context_effort}",
                provider.id
            ));
            if runner::has_direct_generation_adapter(&provider.id) {
                trace.push(format!("adapter direto: {}", provider.id));
            } else {
                trace.push(format!(
                    "adapter direto para {} nao configurado; fallback deterministico",
                    provider.id
                ));
            }
            if !provider.auth_methods.is_empty() {
                let methods = provider
                    .auth_methods
                    .iter()
                    .map(|method| {
                        format!(
                            "{}:{}",
                            method.label,
                            if method.present {
                                "presente"
                            } else {
                                "ausente"
                            }
                        )
                    })
                    .collect::<Vec<_>>()
                    .join(", ");
                trace.push(format!("auth: {methods}"));
            }
        } else {
            trace.push("provider: auto/config".to_string());
            trace.push("adapter: harness deterministico".to_string());
        }
        trace
    }

    fn model_for_stage(&self, provider: &ProviderSelection, stage: Stage) -> String {
        provider
            .stage_models
            .get(stage.key())
            .cloned()
            .unwrap_or_else(|| provider.model.clone())
    }

    fn effort_for_stage(&self, provider: &ProviderSelection, stage: Stage) -> String {
        provider
            .stage_efforts
            .get(stage.key())
            .cloned()
            .unwrap_or_else(|| provider.effort.clone())
    }

    fn stage_file_exists(&self, stage: Stage) -> bool {
        self.active
            .as_ref()
            .map(|active| active.path.join(stage.filename()).exists())
            .unwrap_or(false)
    }

    fn update_running(&mut self, action: Action) {
        // Durante execução só permitimos sair; o resultado chega via on_stage_result.
        if let Action::Quit = action {
            self.should_quit = true;
        }
    }

    fn update_task_execution(&mut self, action: Action) {
        match action {
            Action::Quit => self.should_quit = true,
            Action::Help => self.open_help(),
            Action::Up => self.move_execution_selection(-1),
            Action::Down => self.move_execution_selection(1),
            Action::NextProvider | Action::NextEffort => {
                if let Some(session) = self.execution.as_mut() {
                    session.detail_view = session.detail_view.next();
                }
            }
            Action::PreviousProvider | Action::PreviousEffort => {
                if let Some(session) = self.execution.as_mut() {
                    session.detail_view = session.detail_view.previous();
                }
            }
            Action::Regenerate => {
                if self.execution_has_running_task() {
                    self.footer =
                        Some("Aguarde a task atual terminar antes de retentar".to_string());
                    return;
                }
                if !self.retry_selected_or_first_failed_task() {
                    self.footer = Some("Nenhuma task com erro para retentar".to_string());
                }
            }
            Action::Back => {
                if self.execution_has_running_task() {
                    self.footer =
                        Some("Task em execução; aguarde finalizar ou use q para sair".to_string());
                } else {
                    self.clear_execution_state();
                    self.screen = Screen::StageBoard;
                }
            }
            _ => {}
        }
    }

    fn update_task_decision(&mut self, action: Action, _fx: &mut SideEffects) {
        match action {
            Action::Quit => self.should_quit = true,
            Action::Help => self.open_help(),
            Action::Up => self.move_execution_selection(-1),
            Action::Down => self.move_execution_selection(1),
            Action::NextProvider | Action::NextEffort => {
                if let Some(session) = self.execution.as_mut() {
                    session.detail_view = session.detail_view.next();
                }
            }
            Action::PreviousProvider | Action::PreviousEffort => {
                if let Some(session) = self.execution.as_mut() {
                    session.detail_view = session.detail_view.previous();
                }
            }
            Action::Regenerate => {
                if !self.retry_selected_or_first_failed_task() {
                    self.footer = Some("Nenhuma task com erro para retentar".to_string());
                }
            }
            Action::Approve | Action::Confirm => self.compose_execution_artifact(true),
            Action::Back => {
                self.clear_execution_state();
                self.screen = Screen::StageBoard;
            }
            _ => {}
        }
    }

    fn move_execution_selection(&mut self, delta: isize) {
        let Some(session) = self.execution.as_mut() else {
            return;
        };
        if session.tasks.is_empty() {
            session.selected_index = 0;
            return;
        }
        let len = session.tasks.len() as isize;
        session.selected_index =
            (session.selected_index as isize + delta).clamp(0, len.saturating_sub(1)) as usize;
    }

    fn execution_has_running_task(&self) -> bool {
        self.execution
            .as_ref()
            .is_some_and(|session| session.current_index.is_some())
    }

    fn retry_selected_or_first_failed_task(&mut self) -> bool {
        let Some(index) = self.execution.as_ref().and_then(|session| {
            let selected = session
                .tasks
                .get(session.selected_index)
                .filter(|task| task.status == TaskRunStatus::Failed)
                .map(|_| session.selected_index);
            selected.or_else(|| {
                session
                    .tasks
                    .iter()
                    .position(|task| task.status == TaskRunStatus::Failed)
            })
        }) else {
            return false;
        };

        if let Some(session) = self.execution.as_mut() {
            if let Some(run) = session.tasks.get_mut(index) {
                run.status = TaskRunStatus::Pending;
                run.trace.clear();
                run.usage = None;
                run.result = None;
                run.report.clear();
                run.diff = None;
                run.before = None;
            }
            session.selected_index = index;
        }
        self.start_execution_task_at(index);
        true
    }

    fn update_parallel_running(&mut self, action: Action) {
        match action {
            Action::Quit => self.should_quit = true,
            Action::Help => self.open_help(),
            _ => {}
        }
    }

    fn update_parallel_decision(&mut self, action: Action, fx: &mut SideEffects) {
        match action {
            Action::Regenerate => self.retry_failed_parallel_slots(fx),
            Action::Approve | Action::Confirm => {
                if let Some(stage) = self.parallel_stage {
                    self.compose_parallel_artifact(stage);
                }
            }
            Action::Back | Action::Quit => {
                self.clear_parallel_state();
                self.screen = Screen::StageBoard;
            }
            Action::Help => self.open_help(),
            _ => {}
        }
    }

    fn retry_failed_parallel_slots(&mut self, fx: &mut SideEffects) {
        let Some(stage) = self.parallel_stage else {
            return;
        };
        let active = match &self.active {
            Some(active) => active.clone(),
            None => return,
        };
        let options = self.stage_run_options(true, stage);
        let mut retried = 0usize;
        for slot in &mut self.parallel {
            if slot.status != AgentSlotStatus::Erro {
                continue;
            }
            let task = parallel::TaskRef {
                id: slot.task_id.clone(),
                title: slot.task_label.clone(),
                independent: true,
            };
            let fragment_path = parallel_fragment_path(&active.path, stage, &slot.task_id);
            slot.rx = runner::spawn_parallel_agent(
                self.exe.clone(),
                stage,
                active.name.clone(),
                task,
                self.root.clone(),
                fragment_path.clone(),
                options.clone(),
            );
            slot.status = AgentSlotStatus::Aguardando;
            slot.trace = vec![format!("slot {} retentado", slot.task_id)];
            slot.usage = None;
            slot.result = None;
            slot.fragment_path = fragment_path;
            retried += 1;
        }
        if retried > 0 {
            self.loader.arm(format!(
                "Retentando {retried} AgentSlot(s) de {}…",
                stage.label()
            ));
            self.screen = Screen::ParallelRunning;
            self.footer = None;
            fx.log_parallel_started = Some((stage, retried));
        }
    }

    fn compose_parallel_artifact(&mut self, stage: Stage) {
        let active = match &self.active {
            Some(active) => active.clone(),
            None => return,
        };
        let results = self.collect_parallel_results();
        let artifact = parallel::compose_parallel_artifact(stage, &results);
        let target = active.path.join(stage.filename());
        if let Err(e) = persist::write_atomic(&target, artifact.as_bytes()) {
            self.error = Some(e.to_string());
            self.error_stage = Some(stage);
            self.screen = Screen::ErrorPanel;
            return;
        }
        if let Err(e) = persist::save_stage_state(
            &self.exe,
            &self.root,
            &active.name,
            stage.key(),
            &target,
            "draft",
        ) {
            self.error = Some(e.to_string());
            self.error_stage = Some(stage);
            self.screen = Screen::ErrorPanel;
            return;
        }
        self.reload_active();
        self.clear_parallel_state();
        if self.load_viewer(stage) {
            self.viewer_stage = Some(stage);
            self.screen = Screen::Viewer;
        } else {
            self.error = Some(format!(
                "Artefato paralelo gravado, mas {} não pôde ser relido.",
                stage.filename()
            ));
            self.error_stage = Some(stage);
            self.screen = Screen::ErrorPanel;
        }
    }

    fn collect_parallel_results(&self) -> Vec<SlotResult> {
        self.parallel
            .iter()
            .map(|slot| {
                let fragment = if slot.status == AgentSlotStatus::Concluido {
                    std::fs::read_to_string(&slot.fragment_path)
                        .or_else(|_| {
                            Ok::<_, std::io::Error>(
                                slot.result
                                    .as_ref()
                                    .map(|result| result.stdout.clone())
                                    .unwrap_or_default(),
                            )
                        })
                        .unwrap_or_default()
                } else {
                    String::new()
                };
                SlotResult {
                    task_id: slot.task_id.clone(),
                    task_label: slot.task_label.clone(),
                    status: slot.status,
                    fragment,
                }
            })
            .collect()
    }

    fn clear_parallel_state(&mut self) {
        self.parallel.clear();
        self.parallel_stage = None;
        self.current_usage = None;
        self.current_context = None;
        self.loader.disarm();
    }

    /// Chamado por mod.rs quando o subprocesso da etapa conclui.
    pub fn on_stage_event(&mut self, event: StageEvent) -> Option<StageResult> {
        match event {
            StageEvent::Trace(item) => {
                if self.provider_trace.last() == Some(&item) {
                    return None;
                }
                self.provider_trace.push(item);
                const TRACE_LIMIT: usize = 80;
                if self.provider_trace.len() > TRACE_LIMIT {
                    let drop_count = self.provider_trace.len() - TRACE_LIMIT;
                    self.provider_trace.drain(0..drop_count);
                }
                None
            }
            StageEvent::Usage(usage) => {
                self.current_usage = Some(usage);
                None
            }
            StageEvent::Context(context) => {
                self.current_context = Some(context);
                None
            }
            StageEvent::Finished(result) => Some(result),
        }
    }

    pub fn on_parallel_event(&mut self, slot_index: usize, event: StageEvent) {
        let Some(slot) = self.parallel.get_mut(slot_index) else {
            return;
        };
        match event {
            StageEvent::Trace(item) => {
                if !slot.status.is_terminal() {
                    slot.status = AgentSlotStatus::EmExecucao;
                }
                if slot.trace.last() != Some(&item) {
                    slot.trace.push(item);
                }
                const TRACE_LIMIT: usize = 80;
                if slot.trace.len() > TRACE_LIMIT {
                    let drop_count = slot.trace.len() - TRACE_LIMIT;
                    slot.trace.drain(0..drop_count);
                }
            }
            StageEvent::Usage(usage) => {
                slot.usage = Some(usage.clone());
                self.current_usage = Some(usage);
            }
            StageEvent::Context(context) => {
                self.current_context = Some(context);
            }
            StageEvent::Finished(result) => {
                slot.status = if result.exit_code == 0 {
                    AgentSlotStatus::Concluido
                } else {
                    AgentSlotStatus::Erro
                };
                if slot.usage.is_none() && !result.usage.is_empty() {
                    slot.usage = Some(result.usage.clone());
                }
                if !result.usage.is_empty() {
                    self.session_usage.record(&result.usage);
                    self.last_usage = Some(result.usage.clone());
                }
                if result.context.prompt_tokens_estimate > 0 {
                    self.last_context = Some(result.context.clone());
                }
                slot.trace = result.trace.clone();
                slot.result = Some(result);
            }
        }
    }

    pub fn on_parallel_disconnected(&mut self, slot_index: usize) {
        let Some(slot) = self.parallel.get_mut(slot_index) else {
            return;
        };
        if slot.status.is_terminal() {
            return;
        }
        slot.status = AgentSlotStatus::Erro;
        slot.trace
            .push("canal do AgentSlot desconectado antes de Finished".to_string());
    }

    pub fn parallel_barrier_reached(&self) -> bool {
        !self.parallel.is_empty() && self.parallel.iter().all(|slot| slot.status.is_terminal())
    }

    pub fn parallel_done_count(&self) -> usize {
        self.parallel
            .iter()
            .filter(|slot| slot.status == AgentSlotStatus::Concluido)
            .count()
    }

    pub fn parallel_error_count(&self) -> usize {
        self.parallel
            .iter()
            .filter(|slot| slot.status == AgentSlotStatus::Erro)
            .count()
    }

    pub fn resolve_parallel_barrier(&mut self, fx: &mut SideEffects) {
        if self.screen != Screen::ParallelRunning || !self.parallel_barrier_reached() {
            return;
        }
        self.loader.disarm();
        let Some(stage) = self.parallel_stage else {
            self.screen = Screen::StageBoard;
            return;
        };
        let done = self.parallel_done_count();
        let errors = self.parallel_error_count();
        fx.log_parallel_finished = Some((stage, done, errors));
        if errors > 0 {
            self.footer = Some(
                "Barreira concluída com erros: retente, aceite parcial ou cancele".to_string(),
            );
            self.screen = Screen::ParallelDecision;
            return;
        }
        self.compose_parallel_artifact(stage);
    }

    pub fn on_execution_event(&mut self, event: StageEvent) -> Option<StageResult> {
        match event {
            StageEvent::Trace(item) => {
                let session = self.execution.as_mut()?;
                let index = session.current_index?;
                let run = session.tasks.get_mut(index)?;
                run.status = TaskRunStatus::Running;
                if run.trace.last() != Some(&item) {
                    run.trace.push(item);
                }
                const TRACE_LIMIT: usize = 120;
                if run.trace.len() > TRACE_LIMIT {
                    let drop_count = run.trace.len() - TRACE_LIMIT;
                    run.trace.drain(0..drop_count);
                }
                None
            }
            StageEvent::Usage(usage) => {
                if let Some(session) = self.execution.as_mut() {
                    if let Some(index) = session.current_index {
                        if let Some(run) = session.tasks.get_mut(index) {
                            run.usage = Some(usage.clone());
                        }
                    }
                }
                self.current_usage = Some(usage);
                None
            }
            StageEvent::Context(context) => {
                self.current_context = Some(context);
                None
            }
            StageEvent::Finished(result) => {
                let result_for_log = result.clone();
                self.finish_execution_task(result);
                Some(result_for_log)
            }
        }
    }

    fn finish_execution_task(&mut self, result: StageResult) {
        let (index, before, report_path, active_path) = {
            let Some(session) = self.execution.as_mut() else {
                return;
            };
            let Some(index) = session.current_index else {
                return;
            };
            let Some(active) = self.active.as_ref() else {
                return;
            };
            session.rx = None;
            session.current_index = None;
            let Some(run) = session.tasks.get_mut(index) else {
                return;
            };
            (
                index,
                run.before.take(),
                run.report_path.clone(),
                active.path.clone(),
            )
        };

        let diff = match before {
            Some(before) => execution::capture_workspace_snapshot(&self.root, Some(&active_path))
                .map(|after| execution::diff_snapshots(&before, &after))
                .unwrap_or_else(|e| DiffReport {
                    changed_files: Vec::new(),
                    summary: format!("falha ao capturar snapshot final: {e}"),
                    patch: String::new(),
                }),
            None => DiffReport {
                changed_files: Vec::new(),
                summary: "snapshot inicial indisponível".to_string(),
                patch: String::new(),
            },
        };
        let report = std::fs::read_to_string(&report_path).unwrap_or_default();
        let succeeded = result.exit_code == 0;

        if let Some(session) = self.execution.as_mut() {
            if let Some(run) = session.tasks.get_mut(index) {
                run.status = if succeeded {
                    TaskRunStatus::Succeeded
                } else {
                    TaskRunStatus::Failed
                };
                if run.usage.is_none() && !result.usage.is_empty() {
                    run.usage = Some(result.usage.clone());
                }
                run.trace = result.trace.clone();
                run.report = report;
                run.diff = Some(diff);
                run.result = Some(result.clone());
            }
            session.selected_index = index;
        }

        if !result.usage.is_empty() {
            self.session_usage.record(&result.usage);
            self.last_usage = Some(result.usage.clone());
        }
        if result.context.prompt_tokens_estimate > 0 {
            self.last_context = Some(result.context.clone());
        }
        self.current_usage = None;
        self.current_context = None;

        if succeeded {
            if self.execution_pending_count() > 0 {
                self.start_next_execution_task();
            } else {
                self.compose_execution_artifact(false);
            }
        } else {
            self.loader.disarm();
            self.footer =
                Some("Task falhou: retente, aceite parcial ou cancele a execução".to_string());
            self.screen = Screen::TaskDecision;
        }
    }

    pub fn on_execution_disconnected(&mut self) {
        let Some(session) = self.execution.as_mut() else {
            return;
        };
        let Some(index) = session.current_index.take() else {
            return;
        };
        session.rx = None;
        if let Some(run) = session.tasks.get_mut(index) {
            run.status = TaskRunStatus::Failed;
            run.trace
                .push("canal da task desconectado antes de Finished".to_string());
        }
        session.selected_index = index;
        self.loader.disarm();
        self.footer = Some("Task desconectou antes de concluir".to_string());
        self.screen = Screen::TaskDecision;
    }

    pub fn execution_total_count(&self) -> usize {
        self.execution
            .as_ref()
            .map(|session| session.tasks.len())
            .unwrap_or(0)
    }

    pub fn execution_done_count(&self) -> usize {
        self.execution
            .as_ref()
            .map(|session| {
                session
                    .tasks
                    .iter()
                    .filter(|task| task.status == TaskRunStatus::Succeeded)
                    .count()
            })
            .unwrap_or(0)
    }

    pub fn execution_error_count(&self) -> usize {
        self.execution
            .as_ref()
            .map(|session| {
                session
                    .tasks
                    .iter()
                    .filter(|task| task.status == TaskRunStatus::Failed)
                    .count()
            })
            .unwrap_or(0)
    }

    pub fn execution_pending_count(&self) -> usize {
        self.execution
            .as_ref()
            .map(|session| {
                session
                    .tasks
                    .iter()
                    .filter(|task| task.status == TaskRunStatus::Pending)
                    .count()
            })
            .unwrap_or(0)
    }

    pub fn selected_execution_task(&self) -> Option<&TaskRun> {
        let session = self.execution.as_ref()?;
        session.tasks.get(session.selected_index)
    }

    fn compose_execution_artifact(&mut self, partial: bool) {
        let active = match &self.active {
            Some(active) => active.clone(),
            None => return,
        };
        if partial {
            if let Some(session) = self.execution.as_mut() {
                for run in &mut session.tasks {
                    if matches!(run.status, TaskRunStatus::Pending | TaskRunStatus::Running) {
                        run.status = TaskRunStatus::Skipped;
                        run.trace
                            .push("task pulada ao aceitar resultado parcial".to_string());
                    }
                }
            }
        }
        let tasks = match &self.execution {
            Some(session) => session
                .tasks
                .iter()
                .map(|run| CompletedTask {
                    task: run.task.clone(),
                    status: run.status,
                    report: run.report.clone(),
                    diff: run.diff.clone().unwrap_or_default(),
                    result: run.result.clone(),
                    trace: run.trace.clone(),
                })
                .collect::<Vec<_>>(),
            None => return,
        };
        let git_summary = execution::git_summary(&self.root);
        let artifact = execution::compose_execution_artifact(
            &active.name,
            &tasks,
            git_summary.as_deref(),
            partial,
        );
        let target = active.path.join(Stage::Execution.filename());
        if let Err(e) = persist::write_atomic(&target, artifact.as_bytes()) {
            self.error = Some(e.to_string());
            self.error_stage = Some(Stage::Execution);
            self.screen = Screen::ErrorPanel;
            return;
        }
        if let Err(e) = persist::save_stage_state(
            &self.exe,
            &self.root,
            &active.name,
            Stage::Execution.key(),
            &target,
            "draft",
        ) {
            self.error = Some(e.to_string());
            self.error_stage = Some(Stage::Execution);
            self.screen = Screen::ErrorPanel;
            return;
        }
        self.reload_active();
        self.clear_execution_state();
        if self.load_viewer(Stage::Execution) {
            self.viewer_stage = Some(Stage::Execution);
            self.screen = Screen::Viewer;
        } else {
            self.error = Some(format!(
                "Artefato de execução gravado, mas {} não pôde ser relido.",
                Stage::Execution.filename()
            ));
            self.error_stage = Some(Stage::Execution);
            self.screen = Screen::ErrorPanel;
        }
    }

    fn clear_execution_state(&mut self) {
        self.execution = None;
        self.current_usage = None;
        self.current_context = None;
        self.loader.disarm();
    }

    /// Chamado por mod.rs quando o subprocesso da etapa conclui.
    pub fn on_stage_result(&mut self, res: StageResult) {
        self.loader.disarm();
        self.pending = None;
        self.provider_trace = res.trace.clone();
        let final_usage = self
            .current_usage
            .clone()
            .filter(|usage| !usage.is_empty())
            .or_else(|| {
                if res.usage.is_empty() {
                    None
                } else {
                    Some(res.usage.clone())
                }
            });
        if let Some(usage) = final_usage {
            self.session_usage.record(&usage);
            self.last_usage = Some(usage);
        }
        let final_context = self
            .current_context
            .clone()
            .filter(|context| context.prompt_tokens_estimate > 0)
            .or_else(|| {
                if res.context.prompt_tokens_estimate > 0 {
                    Some(res.context.clone())
                } else {
                    None
                }
            });
        if let Some(context) = final_context {
            self.last_context = Some(context);
        }
        self.current_usage = None;
        self.current_context = None;
        if res.exit_code == 0 {
            if self.load_viewer(res.stage) {
                self.viewer_stage = Some(res.stage);
                self.screen = Screen::Viewer;
            } else {
                self.error = Some(format!(
                    "Etapa concluiu mas o artefato {} não foi encontrado.",
                    res.stage.filename()
                ));
                self.error_stage = Some(res.stage);
                self.screen = Screen::ErrorPanel;
            }
        } else {
            let detail = if res.stderr.trim().is_empty() {
                res.stdout
            } else {
                res.stderr
            };
            self.error = Some(detail);
            self.error_stage = Some(res.stage);
            self.screen = Screen::ErrorPanel;
        }
    }

    /// Lê o artefato da etapa e prepara o viewer (parse Markdown uma vez). T-15.
    fn load_viewer(&mut self, stage: Stage) -> bool {
        let Some(active) = &self.active else {
            return false;
        };
        let path = active.path.join(stage.filename());
        match std::fs::read_to_string(&path) {
            Ok(content) => {
                self.viewer_text = Some(super::markdown::parse(&content));
                self.viewer_raw = content;
                self.viewer_scroll = 0;
                true
            }
            Err(_) => false,
        }
    }

    fn update_viewer(&mut self, action: Action, fx: &mut SideEffects) {
        match action {
            Action::Back => {
                self.screen = Screen::StageBoard;
            }
            Action::Quit => self.should_quit = true,
            Action::Help => self.open_help(),
            Action::Provider => {
                self.provider_return = self.screen;
                self.screen = Screen::ProviderSetup;
            }
            Action::NextProvider => self.quick_cycle_provider(1, fx),
            Action::PreviousProvider => self.quick_cycle_provider(-1, fx),
            Action::NextModel => self.quick_cycle_model(1, fx),
            Action::PreviousModel => self.quick_cycle_model(-1, fx),
            Action::NextEffort => self.quick_cycle_effort(1, fx),
            Action::PreviousEffort => self.quick_cycle_effort(-1, fx),
            Action::Up => {
                self.viewer_scroll = self.viewer_scroll.saturating_sub(1);
            }
            Action::Down => {
                self.viewer_scroll = self.viewer_scroll.saturating_add(1);
            }
            Action::Confirm | Action::Approve => {
                if let Some(stage) = self.viewer_stage {
                    if Self::stage_has_checkpoint(stage) {
                        self.screen = Screen::Checkpoint;
                    } else {
                        self.approve_stage(stage, "approved");
                    }
                }
            }
            Action::Regenerate => {
                if let Some(stage) = self.viewer_stage {
                    fx.log_checkpoint = Some((stage, CheckpointDecision::Regenerate));
                    if stage == Stage::Execution {
                        self.start_execution_stage(true, fx);
                    } else if parallel::supports_parallel(stage) {
                        self.start_parallel_stage(stage, true, fx);
                    } else {
                        self.start_stage(stage, true, fx);
                    }
                }
            }
            _ => {}
        }
    }

    fn update_checkpoint(&mut self, action: Action, fx: &mut SideEffects) {
        let stage = match self.viewer_stage {
            Some(s) => s,
            None => {
                self.screen = Screen::StageBoard;
                return;
            }
        };
        match action {
            Action::Back => self.screen = Screen::Viewer,
            Action::Help => self.open_help(),
            Action::Provider => {
                self.provider_return = self.screen;
                self.screen = Screen::ProviderSetup;
            }
            Action::NextProvider => self.quick_cycle_provider(1, fx),
            Action::PreviousProvider => self.quick_cycle_provider(-1, fx),
            Action::NextModel => self.quick_cycle_model(1, fx),
            Action::PreviousModel => self.quick_cycle_model(-1, fx),
            Action::NextEffort => self.quick_cycle_effort(1, fx),
            Action::PreviousEffort => self.quick_cycle_effort(-1, fx),
            Action::Approve => {
                fx.log_checkpoint = Some((stage, CheckpointDecision::Approve));
                self.approve_stage(stage, "approved");
            }
            Action::Regenerate => {
                fx.log_checkpoint = Some((stage, CheckpointDecision::Regenerate));
                if stage == Stage::Execution {
                    self.start_execution_stage(true, fx);
                } else if parallel::supports_parallel(stage) {
                    self.start_parallel_stage(stage, true, fx);
                } else {
                    self.start_stage(stage, true, fx);
                }
            }
            Action::Edit => {
                fx.log_checkpoint = Some((stage, CheckpointDecision::Edit));
                self.edit_stage(stage);
            }
            _ => {}
        }
    }

    fn approve_stage(&mut self, stage: Stage, state: &str) {
        let active = match &self.active {
            Some(a) => a.clone(),
            None => return,
        };
        let file = active.path.join(stage.filename());
        match persist::save_stage_state(
            &self.exe,
            &self.root,
            &active.name,
            stage.key(),
            &file,
            state,
        ) {
            Ok(()) => {
                self.reload_active();
                if stage == Stage::Memory {
                    self.screen = Screen::Summary;
                } else {
                    // Avança para a próxima etapa no board.
                    self.board_index = (self.board_index + 1).min(Stage::GUIDED.len() - 1);
                    self.screen = Screen::StageBoard;
                }
            }
            Err(e) => {
                self.error = Some(e.to_string());
                self.error_stage = Some(stage);
                self.screen = Screen::ErrorPanel;
            }
        }
    }

    fn edit_stage(&mut self, stage: Stage) {
        let Some(active) = &self.active else { return };
        let file = active.path.join(stage.filename());
        if let Err(e) = editor::open_editor(&file) {
            self.error = Some(e.to_string());
            self.error_stage = Some(stage);
            self.screen = Screen::ErrorPanel;
            return;
        }
        // Recarrega o conteúdo editado e volta ao checkpoint.
        self.load_viewer(stage);
        self.needs_redraw = true;
        self.screen = Screen::Checkpoint;
    }

    fn update_refinement_prompt(&mut self, action: Action, fx: &mut SideEffects) {
        match action {
            Action::Back => self.screen = Screen::StageBoard,
            Action::Help => self.open_help(),
            Action::Provider => {
                self.provider_return = self.screen;
                self.screen = Screen::ProviderSetup;
            }
            Action::NextProvider => self.quick_cycle_provider(1, fx),
            Action::PreviousProvider => self.quick_cycle_provider(-1, fx),
            Action::NextModel => self.quick_cycle_model(1, fx),
            Action::PreviousModel => self.quick_cycle_model(-1, fx),
            Action::NextEffort => self.quick_cycle_effort(1, fx),
            Action::PreviousEffort => self.quick_cycle_effort(-1, fx),
            Action::Regenerate | Action::Confirm => {
                // "r" ou Enter = executar o refinement
                self.start_stage(Stage::Refinement, false, fx);
            }
            Action::Skip => {
                fx.log_checkpoint = Some((Stage::Refinement, CheckpointDecision::Skip));
                self.approve_stage(Stage::Refinement, "skipped");
            }
            _ => {}
        }
    }

    fn update_error(&mut self, action: Action, fx: &mut SideEffects) {
        match action {
            Action::Back => {
                self.error = None;
                self.screen = Screen::StageBoard;
            }
            Action::Quit => self.should_quit = true,
            Action::Help => self.open_help(),
            Action::Regenerate => {
                if let Some(stage) = self.error_stage {
                    self.error = None;
                    if stage == Stage::Execution {
                        self.start_execution_stage(true, fx);
                    } else if parallel::supports_parallel(stage) {
                        self.start_parallel_stage(stage, true, fx);
                    } else {
                        self.start_stage(stage, true, fx);
                    }
                } else {
                    self.screen = Screen::OrchestrationList;
                }
            }
            Action::Edit => {
                let cfg = self.root.join("sdd.config.yaml");
                let _ = editor::open_editor(&cfg);
                self.needs_redraw = true;
            }
            _ => {}
        }
    }

    fn update_help(&mut self, action: Action) {
        if matches!(action, Action::Back | Action::Quit | Action::Help) {
            self.screen = self.help_return;
        }
    }

    fn update_summary(&mut self, action: Action) {
        if matches!(action, Action::Quit | Action::Back | Action::Confirm) {
            self.should_quit = true;
        }
    }
}

/// Reproduz a normalização de slug do harness o suficiente para localizar a
/// orquestração recém-criada (lowercase + hífens). Não precisa ser idêntica:
/// é só um fallback de seleção; o match primário é por `name`.
fn slugify_like(input: &str) -> String {
    let mut out = String::new();
    let mut prev_dash = false;
    for ch in input.chars() {
        if ch.is_ascii_alphanumeric() {
            out.push(ch.to_ascii_lowercase());
            prev_dash = false;
        } else if !prev_dash {
            out.push('-');
            prev_dash = true;
        }
    }
    out.trim_matches('-').to_string()
}

fn load_provider_state(
    root: &std::path::Path,
) -> (
    Vec<ProviderCatalogEntry>,
    Option<ProviderSelection>,
    Option<String>,
) {
    let providers = match provider_catalog_fast(root) {
        Ok(providers) => providers,
        Err(e) => return (Vec::new(), None, Some(e.to_string())),
    };
    let selection = resolve_provider_fast(root, &ProviderResolveOptions::default()).ok();
    (providers, selection, None)
}

fn provider_choice_indices(
    providers: &[ProviderCatalogEntry],
    provider_index: usize,
    selection: Option<&ProviderSelection>,
    stage: Option<Stage>,
) -> (usize, usize) {
    let Some(provider) = providers.get(provider_index) else {
        return (0, 0);
    };
    let selected_model = selection
        .filter(|selection| selection.id == provider.id)
        .and_then(|selection| {
            stage
                .and_then(|stage| selection.stage_models.get(stage.key()).map(String::as_str))
                .or(Some(selection.model.as_str()))
        })
        .or(provider.model.as_deref());
    let model_index = selected_model
        .and_then(|model| provider.models.iter().position(|item| item.id == model))
        .unwrap_or(0);

    let selected_effort = selection
        .filter(|selection| selection.id == provider.id)
        .and_then(|selection| {
            stage
                .and_then(|stage| selection.stage_efforts.get(stage.key()).map(String::as_str))
                .or(Some(selection.effort.as_str()))
        })
        .or(provider.effort.as_deref());
    let effort_index = selected_effort
        .and_then(|effort| provider.efforts.iter().position(|item| item == effort))
        .unwrap_or(0);
    (model_index, effort_index)
}

fn selected_provider_model(provider: &ProviderCatalogEntry, index: usize) -> String {
    provider
        .models
        .get(index.min(provider.models.len().saturating_sub(1)))
        .map(|model| model.id.clone())
        .or_else(|| provider.model.clone())
        .unwrap_or_else(|| "default".to_string())
}

fn selected_provider_effort(provider: &ProviderCatalogEntry, index: usize) -> String {
    provider
        .efforts
        .get(index.min(provider.efforts.len().saturating_sub(1)))
        .cloned()
        .or_else(|| provider.effort.clone())
        .unwrap_or_else(|| "medium".to_string())
}

fn cycle_index(current: usize, len: usize, delta: isize) -> usize {
    if len == 0 {
        return 0;
    }
    let len = len as isize;
    (current as isize + delta).rem_euclid(len) as usize
}

fn parallel_fragment_path(active_path: &std::path::Path, stage: Stage, task_id: &str) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_nanos())
        .unwrap_or(0);
    active_path.join(".sdd-parallel").join(format!(
        "{}-{}-{}-{nanos}.md",
        stage.key(),
        sanitize_fragment_id(task_id),
        std::process::id()
    ))
}

fn execution_report_path(active_path: &std::path::Path, task_id: &str) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_nanos())
        .unwrap_or(0);
    active_path.join(".sdd-execution").join(format!(
        "{}-{}-{nanos}.md",
        std::process::id(),
        sanitize_fragment_id(task_id)
    ))
}

fn sanitize_fragment_id(value: &str) -> String {
    let mut out = String::new();
    for ch in value.chars() {
        if ch.is_ascii_alphanumeric() {
            out.push(ch.to_ascii_lowercase());
        } else if !out.ends_with('-') {
            out.push('-');
        }
    }
    let out = out.trim_matches('-').to_string();
    if out.is_empty() {
        "task".to_string()
    } else {
        out
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::domain::providers::ProviderSelection;
    use std::fs;
    use std::sync::mpsc;

    fn app() -> App {
        App::new(PathBuf::from("."), crate::tui::noop_success_exe())
    }

    fn parallel_handle(id: &str, status: AgentSlotStatus) -> AgentHandle {
        let (_tx, rx) = mpsc::channel();
        AgentHandle {
            task_id: id.to_string(),
            task_label: format!("Task {id}"),
            rx,
            status,
            trace: Vec::new(),
            usage: None,
            result: None,
            fragment_path: PathBuf::from(format!("/tmp/{id}.md")),
        }
    }

    fn stage_result(stage: Stage, exit_code: i32) -> StageResult {
        StageResult {
            stage,
            exit_code,
            stdout: String::new(),
            stderr: String::new(),
            duration_ms: 10,
            trace: vec!["finished".to_string()],
            usage: TokenUsageSample::from_text("entrada", "saida"),
            context: StageContextSample::default(),
        }
    }

    fn codex_selection() -> ProviderSelection {
        ProviderSelection {
            id: "codex".to_string(),
            kind: "cli".to_string(),
            model: "gpt-test".to_string(),
            effort: "high".to_string(),
            offline: false,
            enabled: true,
            auth_env: None,
            auth_present: None,
            auth_methods: Vec::new(),
            models: Vec::new(),
            efforts: Vec::new(),
            stage_models: std::collections::BTreeMap::new(),
            stage_efforts: std::collections::BTreeMap::new(),
            capabilities: Vec::new(),
            token_budget: None,
            usage_limits: Vec::new(),
        }
    }

    fn execution_task(id: &str) -> ExecutionTask {
        ExecutionTask {
            id: id.to_string(),
            title: format!("Task {id}"),
            body: "Editar arquivo de teste".to_string(),
            dependencies: Vec::new(),
            independent: true,
        }
    }

    #[test]
    fn quit_from_list() {
        let mut a = app();
        a.screen = Screen::OrchestrationList;
        // Sair abre um modal de confirmação (não encerra imediatamente).
        a.update(Action::Quit);
        assert_eq!(a.overlay, Overlay::Confirm);
        assert!(!a.should_quit);
        // Com foco em "Sair" (default), Enter confirma a saída.
        a.update(Action::Confirm);
        assert!(a.should_quit);
        assert_eq!(a.overlay, Overlay::None);
    }

    #[test]
    fn quit_confirm_cancel_keeps_running() {
        let mut a = app();
        a.screen = Screen::OrchestrationList;
        a.update(Action::Quit);
        // Alterna o foco para "Cancelar" e confirma: não encerra.
        a.update(Action::NextModel);
        a.update(Action::Confirm);
        assert!(!a.should_quit);
        assert_eq!(a.overlay, Overlay::None);
    }

    #[test]
    fn new_orchestration_enters_text_mode() {
        let mut a = app();
        a.screen = Screen::OrchestrationList;
        a.update(Action::New);
        assert_eq!(a.screen, Screen::NewOrchestration);
        assert!(a.text_mode());
        a.update(Action::Char('o'));
        a.update(Action::Char('i'));
        assert_eq!(a.input, "oi");
        a.update(Action::Backspace);
        assert_eq!(a.input, "o");
    }

    #[test]
    fn help_returns_to_origin() {
        let mut a = app();
        a.screen = Screen::ProviderSetup;
        a.update(Action::Help);
        assert_eq!(a.screen, Screen::Help);
        a.update(Action::Back);
        assert_eq!(a.screen, Screen::ProviderSetup);
    }

    #[test]
    fn welcome_is_initial_screen() {
        let a = app();
        assert_eq!(a.screen, Screen::Welcome);
        assert!(a.text_mode());
    }

    #[test]
    fn slash_menu_opens_from_text_mode_and_filters_commands() {
        let mut a = app();

        a.update(Action::OpenSlashMenu);
        assert_eq!(a.overlay, Overlay::SlashMenu);
        assert!(a.text_mode());

        for ch in "model".chars() {
            a.update(Action::Char(ch));
        }

        assert_eq!(a.slash_query, "model");
        assert_eq!(a.selected_slash_command(), Some(SlashCommandId::Model));
    }

    #[test]
    fn slash_stage_without_active_opens_new_input_for_target_stage() {
        let mut a = app();

        a.update(Action::OpenSlashMenu);
        for ch in "prd".chars() {
            a.update(Action::Char(ch));
        }
        assert_eq!(
            a.selected_slash_command(),
            Some(SlashCommandId::Stage(Stage::Prd))
        );

        a.update(Action::Confirm);

        assert_eq!(a.overlay, Overlay::None);
        assert_eq!(a.screen, Screen::NewOrchestration);
        assert_eq!(a.new_target_stage, Some(Stage::Prd));
        assert!(a.text_mode());
    }

    #[test]
    fn tab_opens_provider_dialog_without_immediate_switch() {
        let mut a = app();
        a.screen = Screen::StageBoard;
        let initial_provider = a.provider_index;

        a.update(Action::NextProvider);

        assert_eq!(a.overlay, Overlay::ProviderDialog);
        assert_eq!(a.provider_index, initial_provider);
    }

    #[test]
    fn provider_dialog_applies_selection_on_confirm() {
        let mut a = app();
        a.screen = Screen::StageBoard;
        a.update(Action::NextProvider);
        a.update(Action::Down);
        let selected_provider = a.providers[a.provider_index].id.clone();

        a.update(Action::Confirm);

        assert_eq!(a.overlay, Overlay::None);
        assert_eq!(
            a.provider_selection
                .as_ref()
                .map(|provider| provider.id.as_str()),
            Some(selected_provider.as_str())
        );
    }

    #[test]
    fn stage_board_model_shortcut_sets_stage_override() {
        let mut a = app();
        let codex_index = a
            .providers
            .iter()
            .position(|provider| provider.id == "codex")
            .unwrap();
        a.provider_index = codex_index;
        a.sync_provider_choice_indices_for_stage(Some(Stage::Techspec));
        a.screen = Screen::StageBoard;
        a.board_index = Stage::GUIDED
            .iter()
            .position(|stage| *stage == Stage::Techspec)
            .unwrap();

        a.update(Action::NextModel);

        let selection = a.provider_selection.as_ref().unwrap();
        assert_eq!(selection.id, "codex");
        assert_eq!(
            selection.stage_models.get(Stage::Techspec.key()),
            Some(&"gpt-5.4".to_string())
        );
        assert!(a
            .footer
            .as_deref()
            .is_some_and(|footer| footer.contains("Tech Spec")));
    }

    #[test]
    fn empty_idea_does_not_submit() {
        let mut a = app();
        a.screen = Screen::OrchestrationList;
        a.update(Action::New);
        a.update(Action::Confirm);
        assert_eq!(a.screen, Screen::NewOrchestration);
        assert!(a.footer.is_some());
    }

    #[test]
    fn provider_confirm_enters_orchestration_list() {
        let mut a = app();
        a.screen = Screen::ProviderSetup;
        a.update(Action::Confirm);
        assert_eq!(a.screen, Screen::OrchestrationList);
        assert!(a.provider_selection.is_some());
    }

    #[test]
    fn provider_confirm_returns_to_origin_screen() {
        let mut a = app();
        a.screen = Screen::StageBoard;
        a.update(Action::Provider);
        assert_eq!(a.screen, Screen::StageBoard);
        assert_eq!(a.overlay, Overlay::ProviderDialog);
        a.update(Action::Confirm);
        assert_eq!(a.screen, Screen::StageBoard);
        assert_eq!(a.overlay, Overlay::None);
        assert!(a.provider_selection.is_some());
    }

    #[test]
    fn stage_result_accumulates_session_usage() {
        let dir = tempfile::tempdir().unwrap();
        let store = dir.path().join("docs/minha-feature");
        fs::create_dir_all(&store).unwrap();
        fs::write(
            store.join("traceability-map.yaml"),
            r#"
orchestration:
  name: "Minha Feature"
  slug: "minha-feature"
  state: draft
artifacts:
  idea:
    file: 01-idea.md
    state: draft
    approved_at: ""
"#,
        )
        .unwrap();
        fs::write(store.join("01-idea.md"), "# Idea\n\n## Rastreabilidade\n").unwrap();

        let mut a = App::new(dir.path().to_path_buf(), crate::tui::noop_success_exe());
        a.refresh_orchestrations();
        a.active = a.orchestrations.first().cloned();
        a.on_stage_result(StageResult {
            stage: Stage::Idea,
            exit_code: 0,
            stdout: String::new(),
            stderr: String::new(),
            duration_ms: 10,
            trace: vec!["provider selecionado: codex".to_string()],
            usage: TokenUsageSample::from_text("entrada", "saida maior"),
            context: StageContextSample::default(),
        });

        assert_eq!(a.session_usage.generation_count, 1);
        assert!(a.session_usage.total_tokens_estimate > 0);
        assert!(a.last_usage.is_some());
    }

    #[test]
    fn stage_usage_event_updates_current_usage_before_finish() {
        let mut a = app();
        let usage = TokenUsageSample::from_provider_usage(100, 0, 20, 0, 120);

        assert!(a.on_stage_event(StageEvent::Usage(usage.clone())).is_none());

        assert_eq!(a.current_usage, Some(usage));
        assert_eq!(a.session_usage.generation_count, 0);
    }

    #[test]
    fn stage_context_event_updates_current_context_before_finish() {
        let mut a = app();
        let context = StageContextSample {
            prompt_tokens_estimate: 900,
            artifact_count: 2,
            ..StageContextSample::default()
        };

        assert!(a
            .on_stage_event(StageEvent::Context(context.clone()))
            .is_none());

        assert_eq!(a.current_context, Some(context));
        assert!(a.last_context.is_none());
    }

    #[test]
    fn pending_stage_with_existing_artifact_opens_viewer() {
        let dir = tempfile::tempdir().unwrap();
        let store = dir.path().join("docs/minha-feature");
        fs::create_dir_all(&store).unwrap();
        fs::write(
            store.join("traceability-map.yaml"),
            r#"
orchestration:
  name: "Minha Feature"
  slug: "minha-feature"
  state: draft
artifacts:
  idea:
    file: 01-idea.md
    state: pending
    approved_at: ""
"#,
        )
        .unwrap();
        fs::write(store.join("01-idea.md"), "# Idea\n\n## Rastreabilidade\n").unwrap();

        let mut a = App::new(dir.path().to_path_buf(), crate::tui::noop_success_exe());
        a.refresh_orchestrations();
        a.active = a.orchestrations.first().cloned();
        a.screen = Screen::StageBoard;
        a.board_index = 0;
        a.update(Action::Confirm);
        assert_eq!(a.screen, Screen::Viewer);
        assert!(a.viewer_raw.contains("# Idea"));
    }

    #[test]
    fn execution_with_empty_tasks_shows_error_instead_of_artifact_fallback() {
        let dir = tempfile::tempdir().unwrap();
        let store = dir.path().join("docs/feat");
        fs::create_dir_all(&store).unwrap();
        fs::write(store.join("04-tasks.md"), "## Backlog\n\nsem tasks\n").unwrap();
        let mut a = App::new(dir.path().to_path_buf(), crate::tui::noop_success_exe());
        a.provider_selection = Some(codex_selection());
        a.active = Some(Orchestration {
            name: "Feat".to_string(),
            slug: "feat".to_string(),
            path: store,
            state: "draft".to_string(),
            stages: Vec::new(),
            pre_stages: Vec::new(),
            post_stages: Vec::new(),
        });
        let mut fx = SideEffects::default();

        a.start_execution_stage(false, &mut fx);

        assert_eq!(a.screen, Screen::ErrorPanel);
        assert!(a
            .error
            .as_deref()
            .is_some_and(|error| error.contains("não contém tasks reconhecidas")));
    }

    #[test]
    fn finished_execution_task_composes_artifact_with_snapshot_diff() {
        let dir = tempfile::tempdir().unwrap();
        let store = dir.path().join("docs/feat");
        let src = dir.path().join("src");
        fs::create_dir_all(&store).unwrap();
        fs::create_dir_all(&src).unwrap();
        fs::write(src.join("a.txt"), "one\n").unwrap();
        let before = execution::capture_workspace_snapshot(dir.path(), Some(&store)).unwrap();
        fs::write(src.join("a.txt"), "one\ntwo\n").unwrap();
        let report_path = store.join(".sdd-execution/report.md");
        fs::create_dir_all(report_path.parent().unwrap()).unwrap();
        fs::write(&report_path, "## Resumo\n\nImplementado.").unwrap();

        let mut a = App::new(dir.path().to_path_buf(), crate::tui::noop_success_exe());
        a.active = Some(Orchestration {
            name: "Feat".to_string(),
            slug: "feat".to_string(),
            path: store.clone(),
            state: "draft".to_string(),
            stages: Vec::new(),
            pre_stages: Vec::new(),
            post_stages: Vec::new(),
        });
        a.screen = Screen::TaskExecution;
        a.execution = Some(ExecutionSession {
            stage: Stage::Execution,
            force: false,
            tasks: vec![TaskRun {
                task: execution_task("TSK-01"),
                status: TaskRunStatus::Running,
                trace: Vec::new(),
                usage: None,
                result: None,
                report_path,
                report: String::new(),
                diff: None,
                before: Some(before),
            }],
            selected_index: 0,
            detail_view: TaskDetailView::Log,
            current_index: Some(0),
            rx: None,
        });

        a.on_execution_event(StageEvent::Finished(stage_result(Stage::Execution, 0)));

        assert_eq!(a.screen, Screen::Viewer);
        let artifact = fs::read_to_string(store.join("06-execution.md")).unwrap();
        assert!(artifact.contains("## Diff por task"));
        assert!(artifact.contains("M src/a.txt"));
    }

    #[test]
    fn start_parallel_stage_populates_slots_for_independent_tasks() {
        let dir = tempfile::tempdir().unwrap();
        let store = dir.path().join("docs/feat");
        fs::create_dir_all(&store).unwrap();
        fs::write(
            store.join("04-tasks.md"),
            "\
## Backlog

| ID | Título | Estimativa | Dependências | Arquivos | Status |
|----|--------|-----------|-------------|----------|--------|
| T-01 | Base | P | — | src/a.rs | Todo |
| T-02 | UI | P | T-01 | src/b.rs | Todo |

### Tasks independentes entre si

- Grupo A: T-02 é independente entre si.
",
        )
        .unwrap();
        let mut a = App::new(dir.path().to_path_buf(), crate::tui::noop_success_exe());
        a.active = Some(Orchestration {
            name: "Feat".to_string(),
            slug: "feat".to_string(),
            path: store,
            state: "draft".to_string(),
            stages: Vec::new(),
            pre_stages: Vec::new(),
            post_stages: Vec::new(),
        });
        let mut fx = SideEffects::default();

        a.start_parallel_stage(Stage::Execution, false, &mut fx);

        assert_eq!(a.screen, Screen::ParallelRunning);
        assert_eq!(a.parallel.len(), 2);
        assert_eq!(fx.log_parallel_started, Some((Stage::Execution, 2)));
    }

    #[test]
    fn on_parallel_event_updates_only_target_slot() {
        let mut a = app();
        a.parallel = vec![
            parallel_handle("T-01", AgentSlotStatus::Aguardando),
            parallel_handle("T-02", AgentSlotStatus::Aguardando),
        ];

        a.on_parallel_event(0, StageEvent::Trace("rodando".to_string()));

        assert_eq!(a.parallel[0].status, AgentSlotStatus::EmExecucao);
        assert_eq!(a.parallel[1].status, AgentSlotStatus::Aguardando);
        assert_eq!(a.parallel[0].trace, vec!["rodando".to_string()]);
    }

    #[test]
    fn parallel_barrier_reached_only_when_all_terminal() {
        let mut a = app();
        a.parallel = vec![
            parallel_handle("T-01", AgentSlotStatus::Concluido),
            parallel_handle("T-02", AgentSlotStatus::EmExecucao),
        ];
        assert!(!a.parallel_barrier_reached());

        a.parallel[1].status = AgentSlotStatus::Erro;
        assert!(a.parallel_barrier_reached());
    }

    #[test]
    fn resolve_parallel_barrier_with_errors_enters_decision() {
        let mut a = app();
        a.screen = Screen::ParallelRunning;
        a.parallel_stage = Some(Stage::Review);
        a.parallel = vec![
            parallel_handle("T-01", AgentSlotStatus::Concluido),
            parallel_handle("T-02", AgentSlotStatus::Erro),
        ];
        let mut fx = SideEffects::default();

        a.resolve_parallel_barrier(&mut fx);

        assert_eq!(a.screen, Screen::ParallelDecision);
        assert_eq!(fx.log_parallel_finished, Some((Stage::Review, 1, 1)));
    }

    #[test]
    fn parallel_decision_cancel_clears_state() {
        let mut a = app();
        a.screen = Screen::ParallelDecision;
        a.parallel_stage = Some(Stage::Execution);
        a.parallel = vec![parallel_handle("T-01", AgentSlotStatus::Erro)];

        a.update(Action::Back);

        assert_eq!(a.screen, Screen::StageBoard);
        assert!(a.parallel.is_empty());
        assert_eq!(a.parallel_stage, None);
    }

    #[test]
    fn parallel_finished_result_records_usage_and_status() {
        let mut a = app();
        a.parallel = vec![parallel_handle("T-01", AgentSlotStatus::EmExecucao)];

        a.on_parallel_event(0, StageEvent::Finished(stage_result(Stage::Execution, 0)));

        assert_eq!(a.parallel[0].status, AgentSlotStatus::Concluido);
        assert!(a.parallel[0].result.is_some());
        assert_eq!(a.session_usage.generation_count, 1);
    }
}
