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 SecretPromptQueue {
49    pub fn new() -> Self {
50        Self {
51            active: None,
52            pending: std::collections::VecDeque::new(),
53        }
54    }
55
56    pub fn poll_requests(
57        &mut self,
58        rx: &Arc<Mutex<tokio::sync::mpsc::UnboundedReceiver<SecretPromptRequest>>>,
59    ) {
60        if let Ok(mut rx) = rx.lock() {
61            while let Ok(req) = rx.try_recv() {
62                self.pending.push_back(req);
63            }
64        }
65        self.activate_next();
66    }
67
68    fn activate_next(&mut self) {
69        if self.active.is_some() {
70            return;
71        }
72        if let Some(req) = self.pending.pop_front() {
73            self.active = Some(PendingSecretPrompt {
74                title: req.title,
75                prompt: req.prompt,
76                buffer: String::new(),
77                response_tx: req.response_tx,
78            });
79        }
80    }
81
82    pub fn is_active(&self) -> bool {
83        self.active.is_some()
84    }
85
86    pub fn active(&self) -> Option<&PendingSecretPrompt> {
87        self.active.as_ref()
88    }
89
90    pub fn push_char(&mut self, ch: char) {
91        if let Some(active) = self.active.as_mut() {
92            active.buffer.push(ch);
93        }
94    }
95
96    pub fn backspace(&mut self) {
97        if let Some(active) = self.active.as_mut() {
98            active.buffer.pop();
99        }
100    }
101
102    pub fn submit(&mut self) {
103        if let Some(mut active) = self.active.take() {
104            let secret = std::mem::take(&mut active.buffer);
105            let _ = active.response_tx.send(Some(secret));
106        }
107        self.activate_next();
108    }
109
110    pub fn cancel(&mut self) {
111        if let Some(mut active) = self.active.take() {
112            active.buffer.clear();
113            let _ = active.response_tx.send(None);
114        }
115        self.activate_next();
116    }
117}