diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index d0cdc63..9dacc38 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -275,8 +275,8 @@ enum Commands { device: String, }, - /// Show a MessageBox asking the user to do something - Prompt { + /// Show a notification to the user (fire-and-forget, no response needed) + Inform { /// Device label device: String, /// Message to display @@ -342,7 +342,7 @@ enum Commands { /// Device label device: String, /// Number of lines - #[arg(long, default_value = "100")] + #[arg(long, default_value = "20")] lines: u32, }, } @@ -537,7 +537,7 @@ fn main() { println!("All windows minimized on {}.", device); } - Commands::Prompt { + Commands::Inform { device, message, title, @@ -547,19 +547,14 @@ fn main() { if let Some(t) = title { body["title"] = json!(t); } - let data = req( + req( &cfg, "POST", - &format!("/devices/{}/prompt", device), + &format!("/devices/{}/inform", device), Some(body), - 30, + 10, ); - let answer = data["answer"].as_str().unwrap_or(""); - if !answer.is_empty() { - println!("{}", answer); - } else { - println!("Prompt confirmed."); - } + println!("User informed on {}.", device); } Commands::Run { diff --git a/crates/client/src/main.rs b/crates/client/src/main.rs index 383431d..ffda017 100644 --- a/crates/client/src/main.rs +++ b/crates/client/src/main.rs @@ -430,6 +430,26 @@ async fn handle_message( } } + ServerMessage::InformRequest { request_id, message, title } => { + let msg = message.clone(); + let ttl = title.clone().unwrap_or_else(|| "Helios".to_string()); + // Fire-and-forget: show MessageBox in background thread, don't block + std::thread::spawn(move || { + #[cfg(windows)] + unsafe { + use windows::core::PCWSTR; + use windows::Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_OK, MB_ICONINFORMATION, HWND_DESKTOP}; + let msg_w: Vec = msg.encode_utf16().chain(std::iter::once(0)).collect(); + let ttl_w: Vec = ttl.encode_utf16().chain(std::iter::once(0)).collect(); + MessageBoxW(HWND_DESKTOP, PCWSTR(msg_w.as_ptr()), PCWSTR(ttl_w.as_ptr()), MB_OK | MB_ICONINFORMATION); + } + #[cfg(not(windows))] + let _ = (msg, ttl); + }); + display::log_ok("inform", &message); + ClientMessage::Ack { request_id } + } + ServerMessage::PromptRequest { request_id, message, title: _ } => { display::prompt_waiting(&message); let answer = tokio::task::spawn_blocking(|| { diff --git a/crates/common/src/protocol.rs b/crates/common/src/protocol.rs index a087b74..94740ad 100644 --- a/crates/common/src/protocol.rs +++ b/crates/common/src/protocol.rs @@ -58,6 +58,12 @@ pub enum ServerMessage { message: String, title: Option, }, + /// Show a non-blocking notification to the user (fire-and-forget) + InformRequest { + request_id: Uuid, + message: String, + title: Option, + }, /// Execute a shell command on the client ExecRequest { request_id: Uuid, diff --git a/crates/server/src/api.rs b/crates/server/src/api.rs index 251e2b9..b89715c 100644 --- a/crates/server/src/api.rs +++ b/crates/server/src/api.rs @@ -401,6 +401,22 @@ pub async fn clipboard_set( } } +/// POST /devices/:label/inform +pub async fn inform_user( + Path(label): Path, + State(state): State, + Json(body): Json, +) -> impl IntoResponse { + match dispatch(&state, &label, "inform", |rid| ServerMessage::InformRequest { + request_id: rid, + message: body.message.clone(), + title: body.title.clone(), + }).await { + Ok(_) => (StatusCode::OK, Json(serde_json::json!({ "ok": true }))).into_response(), + Err(e) => e.into_response(), + } +} + /// POST /devices/:label/prompt #[derive(Deserialize)] pub struct PromptBody { diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index f20e46e..118e64c 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -49,6 +49,7 @@ async fn main() -> anyhow::Result<()> { .route("/devices/:label/screenshot", post(api::request_screenshot)) .route("/devices/:label/exec", post(api::request_exec)) .route("/devices/:label/prompt", post(api::prompt_user)) + .route("/devices/:label/inform", post(api::inform_user)) .route("/devices/:label/windows", get(api::list_windows)) .route("/devices/:label/windows/minimize-all", post(api::minimize_all)) .route("/devices/:label/logs", get(api::logs))