use crate::agents::AgentPool; use crate::io::watcher::WatcherEvent; use crate::state::SessionState; use crate::store::JsonFileStore; use crate::workflow::WorkflowState; use poem::http::StatusCode; use std::sync::Arc; use tokio::sync::{broadcast, mpsc, oneshot}; /// A permission request forwarded from the MCP `prompt_permission` tool to the /// active WebSocket session. The MCP handler blocks on `response_tx` until the /// user approves or denies via the frontend dialog. pub struct PermissionForward { pub request_id: String, pub tool_name: String, pub tool_input: serde_json::Value, pub response_tx: oneshot::Sender, } #[derive(Clone)] pub struct AppContext { pub state: Arc, pub store: Arc, pub workflow: Arc>, pub agents: Arc, /// Broadcast channel for filesystem watcher events. WebSocket handlers /// subscribe to this to push lifecycle notifications to connected clients. pub watcher_tx: broadcast::Sender, /// Sender for permission requests originating from the MCP /// `prompt_permission` tool. The MCP handler sends a [`PermissionForward`] /// and awaits the oneshot response. pub perm_tx: mpsc::UnboundedSender, /// Receiver for permission requests. The active WebSocket handler locks /// this and polls for incoming permission forwards. pub perm_rx: Arc>>, } #[cfg(test)] impl AppContext { pub fn new_test(project_root: std::path::PathBuf) -> Self { let state = SessionState::default(); *state.project_root.lock().unwrap() = Some(project_root.clone()); let store_path = project_root.join(".story_kit_store.json"); let (watcher_tx, _) = broadcast::channel(64); let (perm_tx, perm_rx) = mpsc::unbounded_channel(); Self { state: Arc::new(state), store: Arc::new(JsonFileStore::new(store_path).unwrap()), workflow: Arc::new(std::sync::Mutex::new(WorkflowState::default())), agents: Arc::new(AgentPool::new(3001)), watcher_tx, perm_tx, perm_rx: Arc::new(tokio::sync::Mutex::new(perm_rx)), } } } pub type OpenApiResult = poem::Result; pub fn bad_request(message: String) -> poem::Error { poem::Error::from_string(message, StatusCode::BAD_REQUEST) } #[cfg(test)] mod tests { use super::*; #[test] fn bad_request_returns_400_status() { let err = bad_request("something went wrong".to_string()); assert_eq!(err.status(), StatusCode::BAD_REQUEST); } #[test] fn bad_request_accepts_empty_message() { let err = bad_request(String::new()); assert_eq!(err.status(), StatusCode::BAD_REQUEST); } }