use tokio::sync::mpsc;
use crate::agent::plan::runtime::{
ActivePlan, PlanKickoff, PlanPhaseEvent, PlanPhaseHandle, collect_runner_text,
};
use crate::agent::plan::workflow::{READONLY_PHASE_TOOLS, explore_prompt, plan_prompt};
use crate::provider::AnyAgent;
use crate::ui::avatar::AvatarState;
use crate::ui::colors::c_error;
use crate::ui::slash::SlashCtx;
fn set_busy(ctx: &mut SlashCtx<'_>, busy: bool) -> anyhow::Result<()> {
ctx.renderer.set_avatar_state(if busy {
AvatarState::Thinking
} else {
AvatarState::Idle
});
let status = crate::ui::status::StatusLine::render(
ctx.session,
busy,
0,
None,
ctx.context.current_prompt_name.as_deref(),
None,
ctx.bg_store.as_ref(),
None,
ctx.sandbox.mode.status_badge(),
);
ctx.renderer.draw_bottom(ctx.input, &status, busy)?;
ctx.renderer.render_viewport()?;
*ctx.is_running = busy;
Ok(())
}
pub(crate) async fn cmd_plan(
ctx: &mut SlashCtx<'_>,
parts: &[&str],
_text: &str,
) -> anyhow::Result<()> {
if !ctx.cfg.resolve_phased_workflow_enabled() {
ctx.renderer.write_line(
"/plan is off — set phased_workflow_enabled = true in your config to enable the phased workflow",
c_error(),
)?;
return Ok(());
}
let request = parts.get(1..).map(|p| p.join(" ")).unwrap_or_default();
if request.trim().is_empty() {
ctx.renderer
.write_line("usage: /plan <request>", c_error())?;
return Ok(());
}
let transcript = crate::agent::review::build_transcript(ctx.session);
let cycles = ctx.cfg.resolve_phased_workflow_max_review_cycles();
let agent = ctx.agent.clone();
let (tx, rx) = mpsc::channel::<PlanPhaseEvent>(8);
let task = tokio::spawn(run_phases_task(agent, request, transcript, cycles, tx));
set_busy(ctx, true)?;
if let Some(old) = ctx.plan_phase.take() {
old.task.abort();
}
*ctx.plan_phase = Some(PlanPhaseHandle { rx, task });
Ok(())
}
async fn progress(tx: &mpsc::Sender<PlanPhaseEvent>, text: impl Into<String>, error: bool) -> bool {
tx.send(PlanPhaseEvent::Progress {
text: text.into(),
error,
})
.await
.is_ok()
}
async fn run_phases_task(
agent: AnyAgent,
request: String,
transcript: String,
cycles: usize,
tx: mpsc::Sender<PlanPhaseEvent>,
) {
if !progress(
&tx,
"Phase: Explore — mapping the codebase (read-only)…",
false,
)
.await
{
return;
}
let explore_runner = agent.spawn_phase_runner(
explore_prompt(&request),
transcript.clone(),
READONLY_PHASE_TOOLS,
);
let findings = match collect_runner_text(explore_runner).await {
Ok(t) if !t.trim().is_empty() => t,
Ok(_) => {
progress(&tx, "Phase: Explore — produced no findings; aborting", true).await;
let _ = tx.send(PlanPhaseEvent::Aborted).await;
return;
}
Err(e) => {
progress(&tx, format!("Phase: Explore — error: {e}; aborting"), true).await;
let _ = tx.send(PlanPhaseEvent::Aborted).await;
return;
}
};
if !progress(
&tx,
"Phase: Plan — turning findings into an implementation plan…",
false,
)
.await
{
return;
}
let plan_runner = agent.spawn_phase_runner(
plan_prompt(&request, &findings),
String::new(),
READONLY_PHASE_TOOLS,
);
let plan = match collect_runner_text(plan_runner).await {
Ok(t) if !t.trim().is_empty() => t,
Ok(_) => {
progress(&tx, "Phase: Plan — produced no plan; aborting", true).await;
let _ = tx.send(PlanPhaseEvent::Aborted).await;
return;
}
Err(e) => {
progress(&tx, format!("Phase: Plan — error: {e}; aborting"), true).await;
let _ = tx.send(PlanPhaseEvent::Aborted).await;
return;
}
};
let impl_prompt = format!(
"{request}\n\n--- Implementation plan (from the planning phase) ---\n{plan}\n\n\
Implement this plan now. Make the edits and run the build/tests to verify.",
);
progress(
&tx,
"Phase: Implement — executing the plan (you'll watch it run)…",
false,
)
.await;
let _ = tx
.send(PlanPhaseEvent::Ready(Box::new(PlanKickoff {
impl_prompt,
active: ActivePlan {
plan,
cycles_left: cycles,
},
})))
.await;
}