feat: prompt command (MessageBox), admin status in banner

This commit is contained in:
Helios Agent 2026-03-03 15:44:27 +01:00
parent fdd2124da8
commit e0edf60461
No known key found for this signature in database
GPG key ID: C8259547CD8309B5
6 changed files with 100 additions and 0 deletions

View file

@ -35,4 +35,5 @@ windows = { version = "0.54", features = [
"Win32_UI_Input_KeyboardAndMouse",
"Win32_System_Threading",
"Win32_UI_WindowsAndMessaging",
"Win32_UI_Shell",
] }

View file

@ -23,9 +23,26 @@ mod windows_mgmt;
fn banner() {
println!();
println!(" {} HELIOS REMOTE v{} ({})", "".yellow().bold(), env!("CARGO_PKG_VERSION"), env!("GIT_COMMIT"));
#[cfg(windows)]
{
let admin = is_admin();
let admin_str = if admin {
"admin".green().bold()
} else {
"user (no admin)".yellow()
};
println!(" {} running as {}", "".repeat(20).dimmed(), admin_str);
}
#[cfg(not(windows))]
println!(" {}", "".repeat(45).dimmed());
}
#[cfg(windows)]
fn is_admin() -> bool {
use windows::Win32::UI::Shell::IsUserAnAdmin;
unsafe { IsUserAnAdmin().as_bool() }
}
macro_rules! log_status {
($($arg:tt)*) => {
println!(" {} {}", "".cyan().bold(), format!($($arg)*));
@ -325,6 +342,37 @@ async fn handle_message(
}
}
ServerMessage::PromptRequest { request_id, message, title } => {
let title = title.unwrap_or_else(|| "Helios Remote".to_string());
log_cmd!("prompt {}", &message[..message.len().min(60)]);
#[cfg(windows)]
{
use windows::core::PCWSTR;
use windows::Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_OK, MB_ICONINFORMATION, HWND_DESKTOP};
let msg_wide: Vec<u16> = message.encode_utf16().chain(std::iter::once(0)).collect();
let title_wide: Vec<u16> = title.encode_utf16().chain(std::iter::once(0)).collect();
// Run blocking MessageBox in a thread so we don't block the async runtime
let msg_clone = message.clone();
tokio::task::spawn_blocking(move || {
unsafe {
MessageBoxW(
HWND_DESKTOP,
PCWSTR(msg_wide.as_ptr()),
PCWSTR(title_wide.as_ptr()),
MB_OK | MB_ICONINFORMATION,
);
}
}).await.ok();
log_ok!("User confirmed: {}", &msg_clone[..msg_clone.len().min(40)]);
}
#[cfg(not(windows))]
{
// On non-Windows just log it
println!(" [PROMPT] {}", message);
}
ClientMessage::Ack { request_id }
}
ServerMessage::ExecRequest { request_id, command } => {
// Truncate long commands for display
let cmd_display = if command.len() > 60 {

View file

@ -15,6 +15,14 @@ pub struct WindowInfo {
pub enum ServerMessage {
/// Request a screenshot from the client
ScreenshotRequest { request_id: Uuid },
/// Show a MessageBox on the client asking the user to do something.
/// Blocks until the user clicks OK — use this when you need the user
/// to perform a manual action before continuing.
PromptRequest {
request_id: Uuid,
message: String,
title: Option<String>,
},
/// Execute a shell command on the client
ExecRequest {
request_id: Uuid,

View file

@ -479,6 +479,30 @@ pub async fn clipboard_set(
}
}
/// POST /sessions/:id/prompt
#[derive(Deserialize)]
pub struct PromptBody {
pub message: String,
pub title: Option<String>,
}
pub async fn prompt_user(
Path(session_id): Path<String>,
State(state): State<AppState>,
Json(body): Json<PromptBody>,
) -> impl IntoResponse {
match dispatch(&state, &session_id, "prompt", |rid| ServerMessage::PromptRequest {
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 /sessions/:id/label
#[derive(Deserialize)]
pub struct LabelBody {

View file

@ -51,6 +51,7 @@ async fn main() -> anyhow::Result<()> {
.route("/sessions/:id/click", post(api::request_click))
.route("/sessions/:id/type", post(api::request_type))
.route("/sessions/:id/label", post(api::set_label))
.route("/sessions/:id/prompt", post(api::prompt_user))
.route("/sessions/:id/windows", get(api::list_windows))
.route("/sessions/:id/windows/minimize-all", post(api::minimize_all))
.route("/sessions/:id/windows/:window_id/focus", post(api::focus_window))

View file

@ -287,6 +287,18 @@ def cmd_find_window(args):
print(f"{wid:<20} {title}")
def cmd_prompt(args):
"""Show a MessageBox on the remote PC asking the user to do something.
Blocks until the user clicks OK use this when the AI needs the user
to perform a manual action (e.g. click a button, confirm a dialog)."""
sid = resolve_session(args.session_id)
body = {"message": args.message}
if args.title:
body["title"] = args.title
_req("POST", f"/sessions/{sid}/prompt", json=body)
print(f"User confirmed prompt on session {sid!r}.")
def cmd_run(args):
"""Launch a program on the remote session (fire-and-forget)."""
sid = resolve_session(args.session_id)
@ -374,6 +386,12 @@ def build_parser() -> argparse.ArgumentParser:
fwp.add_argument("session_id")
fwp.add_argument("title", help="Substring to search for (case-insensitive)")
pp = sub.add_parser("prompt", help="Show a MessageBox asking the user to do something manually")
pp.add_argument("session_id")
pp.add_argument("message", help="What to ask the user (e.g. 'Please click Save, then OK')")
pp.add_argument("--title", default=None, help="Dialog title (default: Helios Remote)")
pp.set_defaults(func=cmd_prompt)
rp = sub.add_parser("run", help="Launch a program on the remote session (fire-and-forget)")
rp.add_argument("session_id")
rp.add_argument("program", help="Program to launch (e.g. notepad.exe)")