1use std::collections::BTreeMap;
16use std::sync::atomic::{AtomicU64, Ordering};
17use std::sync::{Arc, Mutex};
18
19#[derive(Clone, Default)]
24pub struct ExtensionStatusMailbox {
25 entries: Arc<Mutex<BTreeMap<String, String>>>,
26 revision: Arc<AtomicU64>,
28}
29
30impl ExtensionStatusMailbox {
31 pub fn new() -> Self {
32 Self::default()
33 }
34
35 pub fn set(&self, key: &str, value: &str) -> bool {
40 let key = key.trim();
41 if key.is_empty() {
42 return false;
43 }
44 let Ok(mut entries) = self.entries.lock() else {
45 return false;
46 };
47 let changed = if value.trim().is_empty() {
48 entries.remove(key).is_some()
49 } else {
50 entries.insert(key.to_string(), value.to_string()) != Some(value.to_string())
51 };
52 if changed {
53 self.revision.fetch_add(1, Ordering::SeqCst);
54 }
55 changed
56 }
57
58 pub fn handle(&self, args: serde_json::Value) -> Result<serde_json::Value, String> {
61 let key = args
62 .get("key")
63 .and_then(serde_json::Value::as_str)
64 .ok_or("set_status requires a string `key`")?;
65 let value = args
66 .get("value")
67 .and_then(serde_json::Value::as_str)
68 .unwrap_or("");
69 let changed = self.set(key, value);
70 Ok(serde_json::json!({ "ok": true, "changed": changed }))
71 }
72
73 pub fn revision(&self) -> u64 {
75 self.revision.load(Ordering::SeqCst)
76 }
77
78 pub fn text(&self) -> String {
80 self.entries
81 .lock()
82 .map(|entries| {
83 entries
84 .values()
85 .filter(|value| !value.trim().is_empty())
86 .cloned()
87 .collect::<Vec<_>>()
88 .join(" ")
89 })
90 .unwrap_or_default()
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn set_and_clear_round_trip() {
100 let mailbox = ExtensionStatusMailbox::new();
101 assert_eq!(mailbox.text(), "");
102 assert!(mailbox.set("langfuse", "langfuse ✓"));
103 assert_eq!(mailbox.text(), "langfuse ✓");
104 assert!(!mailbox.set("langfuse", "langfuse ✓"));
106 assert!(mailbox.set("langfuse", "langfuse ✓ (trace sent)"));
107 assert_eq!(mailbox.text(), "langfuse ✓ (trace sent)");
108 let before = mailbox.revision();
109 assert!(mailbox.set("langfuse", ""));
111 assert_eq!(mailbox.text(), "");
112 assert!(mailbox.revision() > before);
113 assert!(!mailbox.set("langfuse", ""));
115 assert!(!mailbox.set(" ", "ignored"));
116 }
117
118 #[test]
119 fn multiple_extensions_render_in_key_order() {
120 let mailbox = ExtensionStatusMailbox::new();
121 mailbox.set("zeta", "z");
122 mailbox.set("alpha", "a");
123 assert_eq!(mailbox.text(), "a z");
124 }
125
126 #[test]
127 fn handle_parses_the_action_payload() {
128 let mailbox = ExtensionStatusMailbox::new();
129 let out = mailbox
130 .handle(serde_json::json!({"key": "langfuse", "value": "✓"}))
131 .expect("ok");
132 assert_eq!(out["ok"], serde_json::json!(true));
133 assert_eq!(mailbox.text(), "✓");
134 assert!(mailbox.handle(serde_json::json!({"value": "x"})).is_err());
135 }
136}