use crate::approval::types::{PermissionRequest, QuestionRequest};
use crate::state::RecordingState;
use super::VoiceApp;
pub(crate) fn handle_sse_permission_asked(app: &mut VoiceApp, req: PermissionRequest) {
app.approval_queue.add_permission(req);
if app.state != RecordingState::ApprovalPending {
app.state = RecordingState::ApprovalPending;
}
app.render_display();
}
pub(crate) fn handle_sse_permission_replied(
app: &mut VoiceApp,
_session_id: &str,
request_id: &str,
_reply: &str,
) {
app.approval_queue.remove(request_id);
if !app.approval_queue.has_pending() && app.state == RecordingState::ApprovalPending {
app.state = RecordingState::Idle;
}
app.render_display();
}
pub(crate) fn handle_sse_question_asked(app: &mut VoiceApp, req: QuestionRequest) {
app.approval_queue.add_question(req);
if app.state != RecordingState::ApprovalPending {
app.state = RecordingState::ApprovalPending;
}
app.render_display();
}
pub(crate) fn handle_sse_question_replied(
app: &mut VoiceApp,
_session_id: &str,
request_id: &str,
_answers: Vec<Vec<String>>,
) {
app.approval_queue.remove(request_id);
if !app.approval_queue.has_pending() && app.state == RecordingState::ApprovalPending {
app.state = RecordingState::Idle;
}
app.render_display();
}
pub(crate) fn handle_sse_question_rejected(
app: &mut VoiceApp,
_session_id: &str,
request_id: &str,
) {
app.approval_queue.remove(request_id);
if !app.approval_queue.has_pending() && app.state == RecordingState::ApprovalPending {
app.state = RecordingState::Idle;
}
app.render_display();
}
pub(crate) fn handle_sse_session_status(app: &mut VoiceApp, _session_id: &str, busy: bool) {
if busy && app.approval_queue.has_pending() {
app.display
.log("[voice] Session became busy — clearing pending approvals (answered externally).");
app.approval_queue.clear();
if app.state == RecordingState::ApprovalPending {
app.state = RecordingState::Idle;
}
app.render_display();
} else if !busy && app.approval_queue.has_pending() {
app.display
.log("[voice] Session idle — clearing stale approvals.");
app.approval_queue.clear();
if app.state == RecordingState::ApprovalPending {
app.state = RecordingState::Idle;
}
app.render_display();
}
}
pub(crate) fn refresh_approval_display(app: &mut VoiceApp) {
if app.approval_queue.has_pending() {
if app.state == RecordingState::Idle || app.state == RecordingState::ApprovalPending {
app.state = RecordingState::ApprovalPending;
}
} else if app.state == RecordingState::ApprovalPending {
app.state = RecordingState::Idle;
}
app.render_display();
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::VoiceApp;
use crate::approval::types::{PermissionRequest, QuestionRequest};
use crate::config::{AppConfig, ModelSize};
use std::path::PathBuf;
fn test_config() -> AppConfig {
AppConfig {
whisper_model_path: PathBuf::from("/nonexistent/model.bin"),
opencode_port: 4096,
toggle_key: ' ',
model_size: ModelSize::TinyEn,
auto_submit: true,
server_password: None,
data_dir: PathBuf::from("/nonexistent/data"),
audio_device: None,
use_global_hotkey: false,
global_hotkey: "right_option".to_string(),
push_to_talk: true,
handle_prompts: true,
debug: false,
}
}
fn make_permission(id: &str) -> PermissionRequest {
PermissionRequest {
id: id.to_string(),
permission: "bash".to_string(),
metadata: serde_json::Value::Null,
}
}
fn make_question(id: &str) -> QuestionRequest {
QuestionRequest {
id: id.to_string(),
questions: vec![],
}
}
#[test]
fn test_permission_asked_transitions_to_approval_pending() {
let mut app = VoiceApp::new(test_config()).unwrap();
assert_eq!(app.state, RecordingState::Idle);
handle_sse_permission_asked(&mut app, make_permission("p1"));
assert_eq!(app.state, RecordingState::ApprovalPending);
assert!(app.approval_queue.has_pending());
}
#[test]
fn test_permission_replied_removes_from_queue_and_returns_to_idle() {
let mut app = VoiceApp::new(test_config()).unwrap();
handle_sse_permission_asked(&mut app, make_permission("p1"));
assert_eq!(app.state, RecordingState::ApprovalPending);
handle_sse_permission_replied(&mut app, "sess", "p1", "once");
assert_eq!(app.state, RecordingState::Idle);
assert!(!app.approval_queue.has_pending());
}
#[test]
fn test_question_asked_transitions_to_approval_pending() {
let mut app = VoiceApp::new(test_config()).unwrap();
handle_sse_question_asked(&mut app, make_question("q1"));
assert_eq!(app.state, RecordingState::ApprovalPending);
}
#[test]
fn test_question_replied_removes_from_queue() {
let mut app = VoiceApp::new(test_config()).unwrap();
handle_sse_question_asked(&mut app, make_question("q1"));
handle_sse_question_replied(&mut app, "sess", "q1", vec![]);
assert_eq!(app.state, RecordingState::Idle);
assert!(!app.approval_queue.has_pending());
}
#[test]
fn test_question_rejected_removes_from_queue() {
let mut app = VoiceApp::new(test_config()).unwrap();
handle_sse_question_asked(&mut app, make_question("q1"));
handle_sse_question_rejected(&mut app, "sess", "q1");
assert_eq!(app.state, RecordingState::Idle);
assert!(!app.approval_queue.has_pending());
}
#[test]
fn test_session_busy_clears_pending_approvals() {
let mut app = VoiceApp::new(test_config()).unwrap();
handle_sse_permission_asked(&mut app, make_permission("p1"));
handle_sse_question_asked(&mut app, make_question("q1"));
assert_eq!(app.state, RecordingState::ApprovalPending);
assert_eq!(app.approval_queue.len(), 2);
handle_sse_session_status(&mut app, "sess", true);
assert_eq!(app.state, RecordingState::Idle);
assert!(!app.approval_queue.has_pending());
}
#[test]
fn test_session_idle_clears_stale_approvals() {
let mut app = VoiceApp::new(test_config()).unwrap();
handle_sse_permission_asked(&mut app, make_permission("p1"));
assert_eq!(app.state, RecordingState::ApprovalPending);
handle_sse_session_status(&mut app, "sess", false);
assert_eq!(app.state, RecordingState::Idle);
assert!(!app.approval_queue.has_pending());
}
#[test]
fn test_session_busy_no_op_without_pending() {
let mut app = VoiceApp::new(test_config()).unwrap();
assert_eq!(app.state, RecordingState::Idle);
handle_sse_session_status(&mut app, "sess", true);
assert_eq!(app.state, RecordingState::Idle);
}
#[test]
fn test_session_busy_does_not_change_recording_state() {
let mut app = VoiceApp::new(test_config()).unwrap();
handle_sse_permission_asked(&mut app, make_permission("p1"));
app.state = RecordingState::Recording;
handle_sse_session_status(&mut app, "sess", true);
assert!(!app.approval_queue.has_pending());
assert_eq!(app.state, RecordingState::Recording);
}
#[test]
fn test_session_idle_no_op_without_pending() {
let mut app = VoiceApp::new(test_config()).unwrap();
assert_eq!(app.state, RecordingState::Idle);
handle_sse_session_status(&mut app, "sess", false);
assert_eq!(app.state, RecordingState::Idle);
assert!(!app.approval_queue.has_pending());
}
#[test]
fn test_session_idle_does_not_change_transcribing_state() {
let mut app = VoiceApp::new(test_config()).unwrap();
handle_sse_permission_asked(&mut app, make_permission("p1"));
app.state = RecordingState::Transcribing;
handle_sse_session_status(&mut app, "sess", false);
assert!(!app.approval_queue.has_pending());
assert_eq!(app.state, RecordingState::Transcribing);
}
#[test]
fn test_multiple_approvals_stay_pending_until_all_cleared() {
let mut app = VoiceApp::new(test_config()).unwrap();
handle_sse_permission_asked(&mut app, make_permission("p1"));
handle_sse_question_asked(&mut app, make_question("q1"));
assert_eq!(app.approval_queue.len(), 2);
handle_sse_permission_replied(&mut app, "sess", "p1", "once");
assert_eq!(app.state, RecordingState::ApprovalPending);
handle_sse_question_rejected(&mut app, "sess", "q1");
assert_eq!(app.state, RecordingState::Idle);
}
}