collet 0.1.1

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
use super::App;

use crate::tui::state::{PopupKind, PopupState};

impl App {
    pub(super) fn navigate_history_up(&mut self) {
        if self.input_history.is_empty() {
            return;
        }
        match self.history_index {
            None => {
                self.saved_input = self.state.input.clone();
                let idx = self.input_history.len() - 1;
                self.history_index = Some(idx);
                self.state.input = self.input_history[idx].clone();
                self.state.cursor = self.state.input.len();
            }
            Some(idx) if idx > 0 => {
                let new_idx = idx - 1;
                self.history_index = Some(new_idx);
                self.state.input = self.input_history[new_idx].clone();
                self.state.cursor = self.state.input.len();
            }
            _ => {}
        }
    }

    pub(super) fn navigate_history_down(&mut self) {
        if let Some(idx) = self.history_index {
            if idx + 1 < self.input_history.len() {
                let new_idx = idx + 1;
                self.history_index = Some(new_idx);
                self.state.input = self.input_history[new_idx].clone();
                self.state.cursor = self.state.input.len();
            } else {
                self.history_index = None;
                self.state.input = self.saved_input.clone();
                self.state.cursor = self.state.input.len();
            }
        }
    }

    pub(super) fn switch_agent(&mut self, direction: i32) {
        let count = self.config.agents.len();
        if count == 0 {
            return;
        }

        let new_index =
            (self.current_agent_index as i32 + direction).rem_euclid(count as i32) as usize;
        self.current_agent_index = new_index;

        let agent = self.config.agents[new_index].clone();

        // Resolve provider: `providers` fallback chain > single `provider` > model-only.
        // Always reset CLI fields first — they are set only if a CLI provider is selected.
        self.config.cli = None;
        self.config.cli_args = Vec::new();

        // Resolve provider: providers[0] → providers[1] → … → global [default].
        // Each entry is "provider-name/model" or "provider-name".
        let effective_provider: Option<(String, crate::config::ProviderEntry, String, String)> =
            agent
                .providers
                .iter()
                .find_map(|entry_name| {
                    let model = entry_name
                        .find('/')
                        .map(|i| entry_name[i + 1..].to_string())
                        .unwrap_or_else(|| agent.model.clone());
                    crate::config::resolve_provider(entry_name)
                        .map(|(p, k)| (entry_name.clone(), p, k, model))
                })
                .or_else(|| {
                    // Final fallback: global [default] provider/model
                    crate::config::resolve_default_provider()
                });

        let effective_model = effective_provider
            .as_ref()
            .map(|(_, _, _, m)| m.clone())
            .unwrap_or_else(|| agent.model.clone());

        // Extract resolved provider name before the value is consumed by the if-let below.
        let resolved_provider_name = effective_provider
            .as_ref()
            .map(|(_, entry, _, _)| entry.name.clone())
            .or_else(|| agent.provider.clone());

        if let Some((_, entry, api_key, ref model)) = effective_provider {
            if entry.is_cli() {
                // CLI provider — store CLI info; HTTP client stays unchanged (unused).
                self.config.cli = entry.cli.clone();
                self.config.cli_args = entry.cli_args.clone();
                self.client.model = model.clone();
            } else {
                // API provider — full HTTP context switch.
                let profile = crate::api::model_profile::profile_for(model);
                self.client.switch_provider(
                    entry.base_url.clone(),
                    api_key,
                    model.clone(),
                    agent.max_output_tokens.unwrap_or(profile.max_output_tokens),
                );
                self.config.base_url = entry.base_url;
                self.config.api_key = String::new(); // cleared; client holds it
            }
            self.config.supports_tools = agent.supports_tools;
        } else {
            // No provider reference — model-only switch (same provider)
            self.client.model = effective_model.clone();
        }

        self.config.model = effective_model.clone();
        self.state.model_name = effective_model.clone();

        // Update provider_name to match the resolved provider entry.
        // Without this, switching agents keeps the stale provider_name from the
        // previous agent, causing misleading "old_provider/new_model" display
        // and confusing provider/model context in subsequent requests.
        if let Some(ref name) = resolved_provider_name {
            self.state.provider_name = name.clone();
        }

        // Apply global < model < agent resolution chain for runtime params
        self.config.temperature = self.config.resolve_temperature(&agent.model, Some(&agent));
        self.config.thinking_budget_tokens = self
            .config
            .resolve_thinking_budget(&agent.model, Some(&agent));
        self.config.reasoning_effort = self
            .config
            .resolve_reasoning_effort(&agent.model, Some(&agent));

        // Show the actual agent name in the status bar
        self.state.agent_mode = agent.name.clone();

        let display_provider = resolved_provider_name.as_deref().unwrap_or("");
        let status = if display_provider.is_empty() {
            effective_model.clone()
        } else {
            format!("{} / {}", display_provider, effective_model)
        };
        tracing::info!(agent = %agent.name, model = %effective_model, provider = ?resolved_provider_name, index = new_index, total = count, "Switched agent");
        self.state.status_msg = format!("Agent: {} ({})", agent.name, status);
    }

    // ── @mention autocomplete ────────────────────────────────────────────────

    /// Handle a mouse click in the output panel — check for diff "click to expand" regions.
    pub(super) fn handle_output_click(&mut self, column: u16, row: u16) {
        let area = *self.state.last_output_area.borrow();
        // Check if click is within the output area.
        if column < area.x
            || column >= area.x + area.width
            || row < area.y
            || row >= area.y + area.height
        {
            return;
        }

        let click_visual_row = row - area.y;
        let scroll = *self.state.last_render_scroll.borrow();
        let absolute_row = click_visual_row + scroll;

        let cum = self.state.last_line_cum_heights.borrow();
        // Find which line index corresponds to absolute_row.
        // cum[i] = start visual row for line i.
        let line_index = match cum
            .windows(2)
            .enumerate()
            .find(|(_, w)| absolute_row >= w[0] && absolute_row < w[1])
        {
            Some((idx, _)) => idx,
            None => return,
        };

        // Check if this line index matches any click region.
        let regions = self.state.diff_click_regions.borrow();
        if let Some(region) = regions.iter().find(|r| r.line_index == line_index) {
            let title = region.title.clone();
            let content = region.content.clone();
            drop(regions);
            self.state.popup = Some(PopupState {
                title,
                content,
                scroll: 0,
                kind: PopupKind::Info,
                saved_theme: None,
                select_prefix: None,
                search: String::new(),
            });
        }
    }
}