1use std::collections::HashMap;
17use std::sync::{Mutex, OnceLock};
18use std::time::Duration;
19
20use serde_json::{json, Value};
21use tokio::sync::oneshot;
22
23use crate::server::AppState;
24
25pub(crate) const ATTENTION_META_KEY: &str = "browser:attention";
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub(crate) enum Kind {
30 Handoff,
31 Approval,
32 Navigation,
35}
36
37impl Kind {
38 fn as_str(self) -> &'static str {
39 match self {
40 Kind::Handoff => "handoff",
41 Kind::Approval => "approval",
42 Kind::Navigation => "navigation",
43 }
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub(crate) enum Answer {
50 Yes,
52 Cancelled,
53 TimedOut,
54}
55
56impl Answer {
57 pub(crate) fn parse(decision: &str) -> Option<Answer> {
58 match decision {
59 "done" | "approve" => Some(Answer::Yes),
60 "cancel" => Some(Answer::Cancelled),
61 _ => None,
62 }
63 }
64}
65
66struct Pending {
67 block_id: String,
68 kind: Kind,
69 tx: Option<oneshot::Sender<Answer>>,
72}
73
74fn pending() -> &'static Mutex<HashMap<String, Pending>> {
75 static P: OnceLock<Mutex<HashMap<String, Pending>>> = OnceLock::new();
76 P.get_or_init(Default::default)
77}
78
79pub(crate) fn waiting_on_user(block_id: &str) -> Option<Kind> {
84 pending()
85 .lock()
86 .unwrap_or_else(|p| p.into_inner())
87 .values()
88 .find(|p| p.block_id == block_id)
89 .map(|p| p.kind)
90}
91
92pub(crate) fn asking(kind: Kind) -> Vec<String> {
94 pending()
95 .lock()
96 .unwrap_or_else(|p| p.into_inner())
97 .values()
98 .filter(|p| p.kind == kind)
99 .map(|p| p.block_id.clone())
100 .collect()
101}
102
103pub(crate) fn resolve(id: &str, block_id: &str, answer: Answer) -> Result<(), String> {
106 let mut map = pending().lock().unwrap_or_else(|p| p.into_inner());
107 let tx = match map.get_mut(id) {
108 Some(p) if p.block_id == block_id => p.tx.take(),
109 Some(_) => return Err("that request belongs to another pane".to_string()),
110 None => return Err("no such request (it may have timed out)".to_string()),
111 };
112 let Some(tx) = tx else {
113 return Err("that request was already answered".to_string());
114 };
115 let _ = tx.send(answer);
116 Ok(())
117}
118
119pub(crate) fn cancel_for(block_id: &str) {
122 let mut map = pending().lock().unwrap_or_else(|p| p.into_inner());
123 for p in map.values_mut().filter(|p| p.block_id == block_id) {
124 if let Some(tx) = p.tx.take() {
125 let _ = tx.send(Answer::Cancelled);
126 }
127 }
128}
129
130pub(crate) struct Waiting {
135 state: AppState,
136 id: String,
137 block_id: String,
138}
139
140impl Drop for Waiting {
141 fn drop(&mut self) {
142 let mut map = pending().lock().unwrap_or_else(|p| p.into_inner());
146 map.remove(&self.id);
147 clear_banner_if(&self.state, &self.block_id, &self.id);
148 }
149}
150
151fn clear_banner_if(state: &AppState, block_id: &str, id: &str) {
153 let shown = state
154 .mstore
155 .get::<crate::backend::obj::Block>(block_id)
156 .ok()
157 .flatten()
158 .and_then(|b| b.meta.get(ATTENTION_META_KEY).cloned())
159 .unwrap_or(Value::Null);
160 if shown.get("id").and_then(|v| v.as_str()) != Some(id) {
161 return;
162 }
163 let mut clear = crate::backend::obj::MetaMapType::new();
164 clear.insert(ATTENTION_META_KEY.to_string(), Value::Null);
165 let _ = crate::server::http_shell::broadcast_meta_update(state, block_id, &clear);
166}
167
168pub(crate) fn clear_stale(state: &AppState, block_id: &str, id: &str) {
172 let map = pending().lock().unwrap_or_else(|p| p.into_inner());
173 if map.contains_key(id) {
174 return;
175 }
176 clear_banner_if(state, block_id, id);
177}
178
179pub(crate) async fn ask(
184 state: &AppState,
185 block_id: &str,
186 agent_id: &str,
187 kind: Kind,
188 payload: Value,
189 timeout: Duration,
190) -> Result<(Answer, Waiting), String> {
191 let id = uuid::Uuid::new_v4().to_string();
192 let (tx, rx) = oneshot::channel();
193 {
194 let mut map = pending().lock().unwrap_or_else(|p| p.into_inner());
195 if map.values().any(|p| p.block_id == block_id) {
196 return Err("this pane is already waiting for the user".to_string());
197 }
198 map.insert(id.clone(), Pending { block_id: block_id.to_string(), kind, tx: Some(tx) });
199 }
200 let waiting = Waiting { state: state.clone(), id: id.clone(), block_id: block_id.to_string() };
201 let mut banner = json!({ "id": id, "kind": kind.as_str(), "agent": agent_id });
202 if let (Some(b), Some(p)) = (banner.as_object_mut(), payload.as_object()) {
203 for (k, v) in p {
204 b.insert(k.clone(), v.clone());
205 }
206 }
207 let what = ["reason", "what", "origin"]
211 .iter()
212 .find_map(|k| banner.get(*k).and_then(|v| v.as_str()))
213 .unwrap_or("")
214 .to_string();
215 let who = if agent_id.is_empty() { "An agent" } else { agent_id };
216 let text = if what.is_empty() { format!("{who} needs you") } else { format!("{who} needs you: {what}") };
217 let _asking = crate::backend::user_attention::Asking::start(&state.broker, &state.mstore, block_id, "browser", &text);
218 let mut meta = crate::backend::obj::MetaMapType::new();
219 meta.insert(ATTENTION_META_KEY.to_string(), banner);
220 if let Err(e) = crate::server::http_shell::broadcast_meta_update(state, block_id, &meta) {
221 return Err(format!("couldn't show the request in the pane: {e}"));
222 }
223 let answer = match tokio::time::timeout(timeout, rx).await {
224 Ok(Ok(a)) => a,
225 Ok(Err(_)) => Answer::Cancelled,
226 Err(_) => Answer::TimedOut,
227 };
228 Ok((answer, waiting))
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 fn park(id: &str, block: &str, kind: Kind) -> oneshot::Receiver<Answer> {
236 let (tx, rx) = oneshot::channel();
237 pending()
238 .lock()
239 .unwrap()
240 .insert(id.to_string(), Pending { block_id: block.to_string(), kind, tx: Some(tx) });
241 rx
242 }
243
244 #[test]
245 fn decisions_parse() {
246 assert_eq!(Answer::parse("done"), Some(Answer::Yes));
247 assert_eq!(Answer::parse("approve"), Some(Answer::Yes));
248 assert_eq!(Answer::parse("cancel"), Some(Answer::Cancelled));
249 assert_eq!(Answer::parse("yes please"), None);
250 }
251
252 #[tokio::test]
253 async fn resolve_answers_the_waiting_request_once() {
254 let rx = park("attn-test-1", "pane-a", Kind::Approval);
255 resolve("attn-test-1", "pane-a", Answer::Yes).unwrap();
256 assert_eq!(rx.await.unwrap(), Answer::Yes);
257 assert!(resolve("attn-test-1", "pane-a", Answer::Yes).is_err(), "single use");
258 assert_eq!(waiting_on_user("pane-a"), Some(Kind::Approval));
260 pending().lock().unwrap().remove("attn-test-1");
261 }
262
263 #[tokio::test]
264 async fn take_over_cancels_what_the_pane_waits_for() {
265 let rx = park("attn-test-5", "pane-f", Kind::Approval);
266 let _other = park("attn-test-6", "pane-g", Kind::Approval);
267 cancel_for("pane-f");
268 assert_eq!(rx.await.unwrap(), Answer::Cancelled);
269 assert!(resolve("attn-test-5", "pane-f", Answer::Yes).is_err());
271 assert!(resolve("attn-test-6", "pane-g", Answer::Yes).is_ok());
273 pending().lock().unwrap().remove("attn-test-5");
274 pending().lock().unwrap().remove("attn-test-6");
275 }
276
277 #[test]
278 fn a_request_is_answered_only_for_its_own_pane() {
279 let _rx = park("attn-test-2", "pane-b", Kind::Approval);
280 assert!(resolve("attn-test-2", "pane-other", Answer::Yes).is_err());
281 pending().lock().unwrap().remove("attn-test-2");
282 }
283
284 #[test]
285 fn a_pending_request_of_either_kind_locks_the_pane() {
286 let _rx = park("attn-test-3", "pane-c", Kind::Handoff);
287 assert_eq!(waiting_on_user("pane-c"), Some(Kind::Handoff));
288 assert_eq!(waiting_on_user("pane-d"), None);
289 pending().lock().unwrap().remove("attn-test-3");
290 assert_eq!(waiting_on_user("pane-c"), None);
291 let _rx = park("attn-test-4", "pane-e", Kind::Approval);
292 assert_eq!(waiting_on_user("pane-e"), Some(Kind::Approval));
293 pending().lock().unwrap().remove("attn-test-4");
294 }
295}