use tokio::sync::oneshot;
pub use opendev_runtime::PlanDecision;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlanStatus {
Pending,
Approved,
Rejected,
Modified,
}
#[derive(Debug, Clone)]
pub struct PlanAction {
pub label: String,
pub description: String,
pub action: String,
}
pub struct PlanApprovalController {
plan_content: String,
status: PlanStatus,
selected_action: usize,
options: Vec<PlanAction>,
active: bool,
response_tx: Option<oneshot::Sender<PlanDecision>>,
}
impl PlanApprovalController {
pub fn new() -> Self {
Self {
plan_content: String::new(),
status: PlanStatus::Pending,
selected_action: 0,
options: Vec::new(),
active: false,
response_tx: None,
}
}
pub fn active(&self) -> bool {
self.active
}
pub fn plan_content(&self) -> &str {
&self.plan_content
}
pub fn status(&self) -> PlanStatus {
self.status
}
pub fn options(&self) -> &[PlanAction] {
&self.options
}
pub fn selected_action(&self) -> usize {
self.selected_action
}
pub fn start(&mut self, plan_content: String) -> oneshot::Receiver<PlanDecision> {
self.plan_content = plan_content;
self.status = PlanStatus::Pending;
self.selected_action = 0;
self.active = true;
self.options = vec![
PlanAction {
label: "Start implementation".into(),
description: "Auto-approve file edits during implementation.".into(),
action: "approve_auto".into(),
},
PlanAction {
label: "Start implementation (review edits)".into(),
description: "Review each file edit before it's applied.".into(),
action: "approve".into(),
},
PlanAction {
label: "Revise plan".into(),
description: "Stay in plan mode and provide feedback.".into(),
action: "modify".into(),
},
];
let (tx, rx) = oneshot::channel();
self.response_tx = Some(tx);
rx
}
pub fn next(&mut self) {
if !self.active || self.options.is_empty() {
return;
}
self.selected_action = (self.selected_action + 1) % self.options.len();
}
pub fn prev(&mut self) {
if !self.active || self.options.is_empty() {
return;
}
self.selected_action = (self.selected_action + self.options.len() - 1) % self.options.len();
}
pub fn approve(&mut self) -> Option<PlanDecision> {
if !self.active {
return None;
}
self.selected_action = 0;
self.confirm()
}
pub fn reject(&mut self) -> Option<PlanDecision> {
if !self.active {
return None;
}
self.selected_action = self.options.len().saturating_sub(1);
self.confirm()
}
pub fn confirm(&mut self) -> Option<PlanDecision> {
if !self.active || self.options.is_empty() {
return None;
}
let option = &self.options[self.selected_action];
let decision = PlanDecision {
action: option.action.clone(),
feedback: String::new(),
};
self.status = match option.action.as_str() {
"approve_auto" | "approve" => PlanStatus::Approved,
"modify" => PlanStatus::Modified,
_ => PlanStatus::Rejected,
};
if let Some(tx) = self.response_tx.take() {
let _ = tx.send(decision.clone());
}
self.cleanup();
Some(decision)
}
pub fn cancel(&mut self) {
if !self.active {
return;
}
self.selected_action = self.options.len().saturating_sub(1);
self.confirm();
}
fn cleanup(&mut self) {
self.active = false;
self.options.clear();
self.selected_action = 0;
self.plan_content.clear();
self.response_tx = None;
}
}
impl Default for PlanApprovalController {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[path = "plan_approval_tests.rs"]
mod tests;