use crate::command::chat::app::types::PlanDecision;
use crate::command::chat::tools::tool_names;
use std::collections::VecDeque;
use std::sync::{Arc, Condvar, Mutex};
pub struct PendingPlanApproval {
pub agent_name: String,
pub plan_content: String,
pub plan_name: String,
decision: Arc<(Mutex<Option<PlanDecision>>, Condvar)>,
}
impl PendingPlanApproval {
pub fn new(agent_name: String, plan_content: String, plan_name: String) -> Arc<Self> {
Arc::new(Self {
agent_name,
plan_content,
plan_name,
decision: Arc::new((Mutex::new(None), Condvar::new())),
})
}
pub fn wait_for_decision(&self, timeout_secs: u64) -> PlanDecision {
let (lock, cvar) = &*self.decision;
let guard = lock.lock().unwrap_or_else(|e| e.into_inner());
let (mut guard, _timed_out) = cvar
.wait_timeout_while(guard, std::time::Duration::from_secs(timeout_secs), |d| {
d.is_none()
})
.unwrap_or_else(|e| e.into_inner());
if guard.is_none() {
*guard = Some(PlanDecision::Reject);
}
guard.clone().unwrap_or(PlanDecision::Reject)
}
pub fn resolve(&self, decision: PlanDecision) {
let (lock, cvar) = &*self.decision;
let mut d = lock.lock().unwrap_or_else(|e| e.into_inner());
*d = Some(decision);
cvar.notify_one();
}
#[allow(dead_code)]
pub fn is_decided(&self) -> bool {
self.decision
.0
.lock()
.unwrap_or_else(|e| e.into_inner())
.is_some()
}
}
pub struct PlanApprovalQueue {
pending: Mutex<VecDeque<Arc<PendingPlanApproval>>>,
}
impl Default for PlanApprovalQueue {
fn default() -> Self {
Self::new()
}
}
impl PlanApprovalQueue {
pub fn new() -> Self {
Self {
pending: Mutex::new(VecDeque::new()),
}
}
pub fn request_blocking(&self, req: Arc<PendingPlanApproval>) -> PlanDecision {
{
let mut q = self.pending.lock().unwrap_or_else(|e| e.into_inner());
q.push_back(Arc::clone(&req));
}
req.wait_for_decision(120)
}
pub fn pop_pending(&self) -> Option<Arc<PendingPlanApproval>> {
self.pending
.lock()
.unwrap_or_else(|e| e.into_inner())
.pop_front()
}
pub fn deny_all(&self) {
let mut q = self.pending.lock().unwrap_or_else(|e| e.into_inner());
for req in q.drain(..) {
req.resolve(PlanDecision::Reject);
}
}
}
#[derive(Debug)]
struct PlanModeInner {
active: bool,
plan_file_path: Option<String>,
}
#[derive(Debug)]
pub struct PlanModeState {
inner: Mutex<PlanModeInner>,
}
impl Default for PlanModeState {
fn default() -> Self {
Self::new()
}
}
impl PlanModeState {
pub fn new() -> Self {
Self {
inner: Mutex::new(PlanModeInner {
active: false,
plan_file_path: None,
}),
}
}
pub fn is_active(&self) -> bool {
self.inner.lock().map(|g| g.active).unwrap_or(false)
}
pub fn enter(&self, path: impl Into<String>) -> Result<(), String> {
let path = path.into();
match self.inner.lock() {
Ok(mut guard) => {
if guard.active {
return Err("Already in plan mode. Use ExitPlanMode to exit.".to_string());
}
guard.active = true;
guard.plan_file_path = Some(path);
Ok(())
}
Err(e) => Err(format!("Lock poisoned: {}", e)),
}
}
pub fn exit(&self) {
if let Ok(mut guard) = self.inner.lock() {
guard.active = false;
guard.plan_file_path = None;
}
}
pub fn get_state(&self) -> (bool, Option<String>) {
match self.inner.lock() {
Ok(guard) => (guard.active, guard.plan_file_path.clone()),
Err(_) => (false, None),
}
}
pub fn get_plan_file_path(&self) -> Option<String> {
self.inner.lock().ok()?.plan_file_path.clone()
}
}
pub const PLAN_MODE_WHITELIST: &[&str] = &[
tool_names::READ,
tool_names::GLOB,
tool_names::GREP,
tool_names::WEB_FETCH,
tool_names::WEB_SEARCH,
tool_names::ASK,
tool_names::COMPACT,
tool_names::TODO_READ,
tool_names::TODO_WRITE,
tool_names::TASK_OUTPUT,
tool_names::TASK,
tool_names::ENTER_PLAN_MODE,
tool_names::EXIT_PLAN_MODE,
tool_names::ENTER_WORKTREE,
tool_names::EXIT_WORKTREE,
tool_names::AGENT, tool_names::AGENT_TEAM, ];
pub fn is_allowed_in_plan_mode(tool_name: &str) -> bool {
PLAN_MODE_WHITELIST.contains(&tool_name)
}