Skip to main content

synaps_cli/tools/
secret_prompt.rs

1use std::sync::{Arc, Mutex};
2
3/// UI-only secret prompt plumbing for interactive tools.
4///
5/// Secrets sent through this channel are never part of tool parameters, tool
6/// results, chat messages, or API messages. The TUI owns the input UI and sends
7/// only the final secret bytes back to the waiting tool.
8#[derive(Clone)]
9pub struct SecretPromptHandle {
10    tx: tokio::sync::mpsc::UnboundedSender<SecretPromptRequest>,
11}
12
13impl SecretPromptHandle {
14    pub fn new(tx: tokio::sync::mpsc::UnboundedSender<SecretPromptRequest>) -> Self {
15        Self { tx }
16    }
17
18    pub async fn prompt(&self, title: String, prompt: String) -> Option<String> {
19        let (response_tx, response_rx) = tokio::sync::oneshot::channel();
20        let request = SecretPromptRequest {
21            title,
22            prompt,
23            response_tx,
24        };
25        self.tx.send(request).ok()?;
26        response_rx.await.ok().flatten()
27    }
28}
29
30pub struct SecretPromptRequest {
31    pub title: String,
32    pub prompt: String,
33    pub response_tx: tokio::sync::oneshot::Sender<Option<String>>,
34}
35
36pub struct PendingSecretPrompt {
37    pub title: String,
38    pub prompt: String,
39    pub buffer: String,
40    pub response_tx: tokio::sync::oneshot::Sender<Option<String>>,
41}
42
43pub struct SecretPromptQueue {
44    active: Option<PendingSecretPrompt>,
45    pending: std::collections::VecDeque<SecretPromptRequest>,
46}
47
48impl Default for SecretPromptQueue {
49    fn default() -> Self { Self::new() }
50}
51
52impl SecretPromptQueue {
53    pub fn new() -> Self {
54        Self {
55            active: None,
56            pending: std::collections::VecDeque::new(),
57        }
58    }
59
60    pub fn poll_requests(
61        &mut self,
62        rx: &Arc<Mutex<tokio::sync::mpsc::UnboundedReceiver<SecretPromptRequest>>>,
63    ) {
64        if let Ok(mut rx) = rx.lock() {
65            while let Ok(req) = rx.try_recv() {
66                self.pending.push_back(req);
67            }
68        }
69        self.activate_next();
70    }
71
72    fn activate_next(&mut self) {
73        if self.active.is_some() {
74            return;
75        }
76        if let Some(req) = self.pending.pop_front() {
77            self.active = Some(PendingSecretPrompt {
78                title: req.title,
79                prompt: req.prompt,
80                buffer: String::new(),
81                response_tx: req.response_tx,
82            });
83        }
84    }
85
86    pub fn is_active(&self) -> bool {
87        self.active.is_some()
88    }
89
90    pub fn active(&self) -> Option<&PendingSecretPrompt> {
91        self.active.as_ref()
92    }
93
94    pub fn push_char(&mut self, ch: char) {
95        if let Some(active) = self.active.as_mut() {
96            active.buffer.push(ch);
97        }
98    }
99
100    pub fn backspace(&mut self) {
101        if let Some(active) = self.active.as_mut() {
102            active.buffer.pop();
103        }
104    }
105
106    pub fn submit(&mut self) {
107        if let Some(mut active) = self.active.take() {
108            let secret = std::mem::take(&mut active.buffer);
109            let _ = active.response_tx.send(Some(secret));
110        }
111        self.activate_next();
112    }
113
114    pub fn cancel(&mut self) {
115        if let Some(mut active) = self.active.take() {
116            active.buffer.clear();
117            let _ = active.response_tx.send(None);
118        }
119        self.activate_next();
120    }
121}