opencode_voice/app/
approval.rs1use crate::approval::types::{PermissionRequest, QuestionRequest};
12use crate::state::RecordingState;
13
14use super::VoiceApp;
15
16pub(crate) fn handle_sse_permission_asked(app: &mut VoiceApp, req: PermissionRequest) {
21 app.approval_queue.add_permission(req);
22 if app.state != RecordingState::ApprovalPending {
23 app.state = RecordingState::ApprovalPending;
24 }
25 app.render_display();
26}
27
28pub(crate) fn handle_sse_permission_replied(
34 app: &mut VoiceApp,
35 _session_id: &str,
36 request_id: &str,
37 _reply: &str,
38) {
39 app.approval_queue.remove(request_id);
40 if !app.approval_queue.has_pending() && app.state == RecordingState::ApprovalPending {
41 app.state = RecordingState::Idle;
42 }
43 app.render_display();
44}
45
46pub(crate) fn handle_sse_question_asked(app: &mut VoiceApp, req: QuestionRequest) {
51 app.approval_queue.add_question(req);
52 if app.state != RecordingState::ApprovalPending {
53 app.state = RecordingState::ApprovalPending;
54 }
55 app.render_display();
56}
57
58pub(crate) fn handle_sse_question_replied(
64 app: &mut VoiceApp,
65 _session_id: &str,
66 request_id: &str,
67 _answers: Vec<Vec<String>>,
68) {
69 app.approval_queue.remove(request_id);
70 if !app.approval_queue.has_pending() && app.state == RecordingState::ApprovalPending {
71 app.state = RecordingState::Idle;
72 }
73 app.render_display();
74}
75
76pub(crate) fn handle_sse_question_rejected(
82 app: &mut VoiceApp,
83 _session_id: &str,
84 request_id: &str,
85) {
86 app.approval_queue.remove(request_id);
87 if !app.approval_queue.has_pending() && app.state == RecordingState::ApprovalPending {
88 app.state = RecordingState::Idle;
89 }
90 app.render_display();
91}
92
93pub(crate) fn refresh_approval_display(app: &mut VoiceApp) {
106 if app.approval_queue.has_pending() {
107 if app.state == RecordingState::Idle || app.state == RecordingState::ApprovalPending {
108 app.state = RecordingState::ApprovalPending;
109 }
110 } else if app.state == RecordingState::ApprovalPending {
111 app.state = RecordingState::Idle;
112 }
113 app.render_display();
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::app::VoiceApp;
120 use crate::approval::types::{PermissionRequest, QuestionRequest};
121 use crate::config::{AppConfig, ModelSize};
122 use std::path::PathBuf;
123
124 fn test_config() -> AppConfig {
125 AppConfig {
126 whisper_model_path: PathBuf::from("/nonexistent/model.bin"),
127 opencode_port: 4096,
128 toggle_key: ' ',
129 model_size: ModelSize::TinyEn,
130 auto_submit: true,
131 server_password: None,
132 data_dir: PathBuf::from("/nonexistent/data"),
133 audio_device: None,
134 use_global_hotkey: false,
135 global_hotkey: "right_option".to_string(),
136 push_to_talk: true,
137 approval_mode: true,
138 }
139 }
140
141 fn make_permission(id: &str) -> PermissionRequest {
142 PermissionRequest {
143 id: id.to_string(),
144 permission: "bash".to_string(),
145 metadata: serde_json::Value::Null,
146 }
147 }
148
149 fn make_question(id: &str) -> QuestionRequest {
150 QuestionRequest {
151 id: id.to_string(),
152 questions: vec![],
153 }
154 }
155
156 #[test]
157 fn test_permission_asked_transitions_to_approval_pending() {
158 let mut app = VoiceApp::new(test_config()).unwrap();
159 assert_eq!(app.state, RecordingState::Idle);
160 handle_sse_permission_asked(&mut app, make_permission("p1"));
161 assert_eq!(app.state, RecordingState::ApprovalPending);
162 assert!(app.approval_queue.has_pending());
163 }
164
165 #[test]
166 fn test_permission_replied_removes_from_queue_and_returns_to_idle() {
167 let mut app = VoiceApp::new(test_config()).unwrap();
168 handle_sse_permission_asked(&mut app, make_permission("p1"));
169 assert_eq!(app.state, RecordingState::ApprovalPending);
170
171 handle_sse_permission_replied(&mut app, "sess", "p1", "once");
172 assert_eq!(app.state, RecordingState::Idle);
173 assert!(!app.approval_queue.has_pending());
174 }
175
176 #[test]
177 fn test_question_asked_transitions_to_approval_pending() {
178 let mut app = VoiceApp::new(test_config()).unwrap();
179 handle_sse_question_asked(&mut app, make_question("q1"));
180 assert_eq!(app.state, RecordingState::ApprovalPending);
181 }
182
183 #[test]
184 fn test_question_replied_removes_from_queue() {
185 let mut app = VoiceApp::new(test_config()).unwrap();
186 handle_sse_question_asked(&mut app, make_question("q1"));
187 handle_sse_question_replied(&mut app, "sess", "q1", vec![]);
188 assert_eq!(app.state, RecordingState::Idle);
189 assert!(!app.approval_queue.has_pending());
190 }
191
192 #[test]
193 fn test_question_rejected_removes_from_queue() {
194 let mut app = VoiceApp::new(test_config()).unwrap();
195 handle_sse_question_asked(&mut app, make_question("q1"));
196 handle_sse_question_rejected(&mut app, "sess", "q1");
197 assert_eq!(app.state, RecordingState::Idle);
198 assert!(!app.approval_queue.has_pending());
199 }
200
201 #[test]
202 fn test_multiple_approvals_stay_pending_until_all_cleared() {
203 let mut app = VoiceApp::new(test_config()).unwrap();
204 handle_sse_permission_asked(&mut app, make_permission("p1"));
205 handle_sse_question_asked(&mut app, make_question("q1"));
206 assert_eq!(app.approval_queue.len(), 2);
207
208 handle_sse_permission_replied(&mut app, "sess", "p1", "once");
209 assert_eq!(app.state, RecordingState::ApprovalPending);
211
212 handle_sse_question_rejected(&mut app, "sess", "q1");
213 assert_eq!(app.state, RecordingState::Idle);
215 }
216}