Skip to main content

opendev_web/state/
approvals.rs

1//! Approval, ask-user, and plan-approval oneshot-based request handling.
2
3use tokio::sync::oneshot;
4
5use super::{
6    AppState, ApprovalResult, AskUserResult, PendingApproval, PendingApprovalSlot, PendingAskUser,
7    PendingAskUserSlot, PendingPlanApproval, PendingPlanApprovalSlot, PlanApprovalResult,
8};
9
10impl AppState {
11    // --- Approvals (oneshot-based) ---
12
13    /// Add a pending approval request.
14    ///
15    /// Returns a `oneshot::Receiver` that the caller can `.await` to block
16    /// until the approval is resolved (or the state is torn down / interrupted).
17    pub async fn add_pending_approval(
18        &self,
19        id: String,
20        approval: PendingApproval,
21    ) -> oneshot::Receiver<ApprovalResult> {
22        let (tx, rx) = oneshot::channel();
23        self.inner.pending_approvals.lock().await.insert(
24            id,
25            PendingApprovalSlot {
26                meta: approval,
27                tx: Some(tx),
28            },
29        );
30        rx
31    }
32
33    /// Resolve a pending approval by sending through the oneshot channel.
34    ///
35    /// Returns the approval metadata if found, `None` if not found or already resolved.
36    pub async fn resolve_approval(
37        &self,
38        id: &str,
39        approved: bool,
40        auto_approve: bool,
41    ) -> Option<PendingApproval> {
42        let mut approvals = self.inner.pending_approvals.lock().await;
43        if let Some(mut slot) = approvals.remove(id) {
44            if let Some(tx) = slot.tx.take() {
45                let _ = tx.send(ApprovalResult {
46                    approved,
47                    auto_approve,
48                });
49            }
50            Some(slot.meta)
51        } else {
52            None
53        }
54    }
55
56    /// Get metadata for a pending approval (without resolving it).
57    pub async fn get_pending_approval(&self, id: &str) -> Option<PendingApproval> {
58        self.inner
59            .pending_approvals
60            .lock()
61            .await
62            .get(id)
63            .map(|slot| slot.meta.clone())
64    }
65
66    /// Clear all pending approvals for a session (e.g. when session ends).
67    ///
68    /// Sends rejection through the oneshot channels so any blocked agent
69    /// tasks wake up rather than hanging forever.
70    pub async fn clear_session_approvals(&self, session_id: &str) {
71        let mut approvals = self.inner.pending_approvals.lock().await;
72        let to_remove: Vec<String> = approvals
73            .iter()
74            .filter(|(_, slot)| slot.meta.session_id.as_deref() == Some(session_id))
75            .map(|(id, _)| id.clone())
76            .collect();
77
78        for id in to_remove {
79            if let Some(mut slot) = approvals.remove(&id)
80                && let Some(tx) = slot.tx.take()
81            {
82                let _ = tx.send(ApprovalResult {
83                    approved: false,
84                    auto_approve: false,
85                });
86            }
87        }
88    }
89
90    // --- Ask-user (oneshot-based) ---
91
92    /// Add a pending ask-user request.
93    ///
94    /// Returns a `oneshot::Receiver` that the agent can `.await`.
95    pub async fn add_pending_ask_user(
96        &self,
97        id: String,
98        ask_user: PendingAskUser,
99    ) -> oneshot::Receiver<AskUserResult> {
100        let (tx, rx) = oneshot::channel();
101        self.inner.pending_ask_users.lock().await.insert(
102            id,
103            PendingAskUserSlot {
104                meta: ask_user,
105                tx: Some(tx),
106            },
107        );
108        rx
109    }
110
111    /// Resolve a pending ask-user request.
112    pub async fn resolve_ask_user(
113        &self,
114        id: &str,
115        answers: Option<serde_json::Value>,
116        cancelled: bool,
117    ) -> Option<PendingAskUser> {
118        let mut ask_users = self.inner.pending_ask_users.lock().await;
119        if let Some(mut slot) = ask_users.remove(id) {
120            if let Some(tx) = slot.tx.take() {
121                let _ = tx.send(AskUserResult { answers, cancelled });
122            }
123            Some(slot.meta)
124        } else {
125            None
126        }
127    }
128
129    /// Get metadata for a pending ask-user request.
130    pub async fn get_pending_ask_user(&self, id: &str) -> Option<PendingAskUser> {
131        self.inner
132            .pending_ask_users
133            .lock()
134            .await
135            .get(id)
136            .map(|slot| slot.meta.clone())
137    }
138
139    // --- Plan approval (oneshot-based) ---
140
141    /// Add a pending plan approval request.
142    ///
143    /// Returns a `oneshot::Receiver` that the agent can `.await` to block
144    /// until the plan is approved, rejected, or revised.
145    pub async fn add_pending_plan_approval(
146        &self,
147        id: String,
148        plan_approval: PendingPlanApproval,
149    ) -> oneshot::Receiver<PlanApprovalResult> {
150        let (tx, rx) = oneshot::channel();
151        self.inner.pending_plan_approvals.lock().await.insert(
152            id,
153            PendingPlanApprovalSlot {
154                meta: plan_approval,
155                tx: Some(tx),
156            },
157        );
158        rx
159    }
160
161    /// Resolve a pending plan approval.
162    ///
163    /// `action` is typically "approve", "reject", or "revise".
164    /// `feedback` is optional textual feedback from the user.
165    ///
166    /// Returns the plan-approval metadata if found, `None` if already resolved.
167    pub async fn resolve_plan_approval(
168        &self,
169        id: &str,
170        action: String,
171        feedback: String,
172    ) -> Option<PendingPlanApproval> {
173        let mut plan_approvals = self.inner.pending_plan_approvals.lock().await;
174        if let Some(mut slot) = plan_approvals.remove(id) {
175            if let Some(tx) = slot.tx.take() {
176                let _ = tx.send(PlanApprovalResult { action, feedback });
177            }
178            Some(slot.meta)
179        } else {
180            None
181        }
182    }
183
184    /// Get metadata for a pending plan approval.
185    pub async fn get_pending_plan_approval(&self, id: &str) -> Option<PendingPlanApproval> {
186        self.inner
187            .pending_plan_approvals
188            .lock()
189            .await
190            .get(id)
191            .map(|slot| slot.meta.clone())
192    }
193
194    /// Clear all pending plan approvals for a session.
195    pub async fn clear_session_plan_approvals(&self, session_id: &str) {
196        let mut plan_approvals = self.inner.pending_plan_approvals.lock().await;
197        let to_remove: Vec<String> = plan_approvals
198            .iter()
199            .filter(|(_, slot)| slot.meta.session_id.as_deref() == Some(session_id))
200            .map(|(id, _)| id.clone())
201            .collect();
202
203        for id in to_remove {
204            if let Some(mut slot) = plan_approvals.remove(&id)
205                && let Some(tx) = slot.tx.take()
206            {
207                let _ = tx.send(PlanApprovalResult {
208                    action: "reject".to_string(),
209                    feedback: "Session ended".to_string(),
210                });
211            }
212        }
213    }
214}