Skip to main content

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