collet 0.1.1

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
use super::App;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;

use crate::agent::r#loop::AgentEvent;
use crate::tui::state::{ChatMessage, MessageRole, PendingPlan};

impl App {
    /// Save the pending architect plan to `docs/plan/` as a markdown file.
    /// Returns the saved filename on success.
    pub(super) fn save_plan_to_file(&self, plan: &PendingPlan) -> Result<String, String> {
        let plan_dir = std::path::Path::new(&self.working_dir)
            .join("docs")
            .join("plan");
        if let Err(e) = std::fs::create_dir_all(&plan_dir) {
            return Err(format!("Failed to create docs/plan/: {e}"));
        }

        let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
        let filename = format!("plan_{timestamp}.md");
        let path = plan_dir.join(&filename);

        let content = format!(
            "# Implementation Plan\n\n\
             > Task: {}\n\n\
             ---\n\n\
             {}\n",
            plan.user_msg, plan.plan,
        );

        std::fs::write(&path, &content).map_err(|e| format!("Failed to save plan: {e}"))?;
        Ok(format!("docs/plan/{filename}"))
    }

    /// Save the pending architect plan to a markdown file.
    pub(super) fn handle_save_plan(&mut self) {
        if let Some(ref plan) = self.state.pending_plan {
            match self.save_plan_to_file(plan) {
                Ok(rel_path) => {
                    self.state.messages.push(ChatMessage::text(
                        MessageRole::System,
                        format!(
                            "Plan saved to `{rel_path}`.\n\n\
                             `/proceed`      — start code agent now\n\
                             `/cancel-plan`  — discard the plan"
                        ),
                    ));
                }
                Err(e) => {
                    self.state
                        .messages
                        .push(ChatMessage::text(MessageRole::System, e));
                }
            }
        } else {
            self.state.messages.push(ChatMessage::text(
                MessageRole::System,
                "No pending plan. Run a task in `/architect` mode first.",
            ));
        }
    }

    /// Start code phase using the pending architect plan.
    pub(super) fn handle_proceed_plan(&mut self, event_tx: mpsc::UnboundedSender<AgentEvent>) {
        if let Some(plan) = self.state.pending_plan.take() {
            // Ensure plan is saved before handing off to code agent.
            if let Err(e) = self.save_plan_to_file(&plan) {
                self.state
                    .messages
                    .push(ChatMessage::text(MessageRole::System, e));
            }
            self.state
                .messages
                .push(ChatMessage::text(MessageRole::User, "/proceed"));
            self.state.agent_busy = true;
            self.agent_busy_since = Some(std::time::Instant::now());
            self.state.elapsed_secs = 0;
            self.state.status_msg = "Thinking (code)...".to_string();
            self.state.agent_mode = "code".to_string();
            self.state.scroll_offset = 0;
            self.scroll_target = 0;

            // Begin checkpoint
            self.checkpoint_mgr.begin(&plan.user_msg);

            if let Some(ref ctx) = self.context {
                self.last_context_backup = Some(ctx.clone());
            }

            let client = self.client.clone();
            let config = self.config.clone();
            let working_dir = self.working_dir.clone();
            let system_prompt = plan.system_prompt.clone();
            let plan_text = plan.plan.clone();
            let user_msg = plan.user_msg.clone();
            let lsp_manager = self.lsp_manager.clone();
            // Pass the architect's completed context so the code agent can skip
            // re-reading files that were already examined during planning.
            let arch_context = self.context.clone();

            let cancel = CancellationToken::new();
            self.cancel_token = Some(cancel.clone());

            // Build approval gate for code phase — reuse session cache and channel.
            let (approval_req_tx, approval_req_rx) = tokio::sync::mpsc::unbounded_channel();
            self.approval_req_rx = Some(approval_req_rx);
            let approval_gate = crate::agent::approval::ApprovalGate::new(
                self.approve_mode.clone(),
                approval_req_tx,
            )
            .with_session_approvals(self.session_approvals.clone());

            tokio::spawn(async move {
                crate::agent::r#loop::execute_plan(crate::agent::r#loop::ExecutePlanParams {
                    client,
                    config,
                    system_prompt,
                    plan: plan_text,
                    user_msg,
                    working_dir,
                    event_tx,
                    cancel,
                    lsp_manager,
                    arch_context,
                    approval_gate,
                })
                .await;
            });
        } else {
            self.state.messages.push(ChatMessage::text(
                MessageRole::System,
                "No pending plan. Run a task in `/architect` mode first.",
            ));
        }
    }

    /// Discard the pending architect plan.
    pub(super) fn handle_cancel_plan(&mut self) {
        if self.state.pending_plan.take().is_some() {
            self.state
                .messages
                .push(ChatMessage::text(MessageRole::System, "Plan discarded."));
        } else {
            self.state.messages.push(ChatMessage::text(
                MessageRole::System,
                "No pending plan to cancel.",
            ));
        }
    }
}