app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
master
John Lauer
v2.0.720 (insiders): Groundwork for the macOS build of the Rust pup (Kyle's pull request #3, pup-bridge#128): the Mac h…
87f5051
27d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
//! OS-window etiquette verbs: raise (gated on a reason, toast + caption first), lower, alert
//! (taskbar flash), focus (alias of raise), progress (taskbar progress bar). Everything the
//! desktop does here is an ab verb; the host only re-asserts "bottom" in-process.
use crate::{resolve, touch, Ctx};
use pup_core::{err, ok, Caller};
use serde_json::{json, Value};
use std::time::Duration;
const AB_T: Duration = Duration::from_secs(8);
pub fn hwnd_of_pub(ctx: &Ctx, s: &pup_session::Session) -> Option<i64> { hwnd_of(ctx, s) }
fn hwnd_of(ctx: &Ctx, s: &pup_session::Session) -> Option<i64> {
s.hwnd.filter(|&h| ctx.host.is_alive(h)).or_else(|| {
let tag = s.tag_string();
ctx.host.windows_for_pids(&[], Some("Chrome_WidgetWin_1")).into_iter().find(|w| ctx.host.has_tag(w.hwnd, &tag)).map(|w| { ctx.sessions.with_mut(&s.id, |x| x.hwnd = Some(w.hwnd)); w.hwnd })
})
}
pub fn raise(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {
let s = match resolve(ctx, caller, args) { Ok(s) => s, Err(e) => return e };
let reason = args.get("foregroundReason").and_then(|r| r.as_str()).map(|r| r.trim()).filter(|r| r.len() >= 8);
let Some(reason) = reason else {
return pup_core::merge(pup_core::merge(err("foreground_reason_required", "Raise refused: foregroundReason is required.", Some("pup windows stay in the BACKGROUND by default. Pass foregroundReason:\"<why the user asked to see this window>\"; it is captioned on their screen. For a nudge without stealing focus use pup_alert_window.")), json!({ "window": s.id, "sessionId": s.id })), json!({ "ok": false, "output": "" }));
};
let Some(h) = hwnd_of(ctx, &s) else { return err("no_window_handle", "pup has no OS window handle for this window yet", Some("pup_rescan {} re-resolves handles; if the window exists it will be tagged again.")) };
if ctx.host.foreground() == h {
return ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "focused": true, "placement": "foreground", "windowEtiquette": "foreground", "alreadyForeground": true, "reason": reason, "_hint": "This window is already the user's foreground window; nothing to do." }));
}
let thread = if caller.thread.is_empty() { "an AI thread".to_string() } else { caller.thread.clone() };
if ctx.settings.get("fgCaption", "on") != "off" {
let ms: u64 = ctx.settings.get("fgCaptionMs", "3500").parse().unwrap_or(3500);
let _ = ctx.ab.call("desktop_caption", json!({ "text": format!("{thread}: {reason}"), "duration": ms, "expiresInMs": ms, "position": "bottom", "size": "medium", "id": format!("pup-fg-{}", s.id) }), caller, AB_T);
}
// 2.0.604 (John, 2026-09-17: "why do you keep shoving windows in front of me without warning me?
// that's super rude" / "i realize you just put an ab toast up warning me, but that was like a minute
// after you shoved the window in my face while i was busy typing in another window"). The toast used to
// carry focus:false, so clicking it did nothing, and it landed in the same instant as the window, which
// is not a warning at all. Now it CARRIES THE WINDOW (a click raises it) and the raise waits a beat
// behind it, so the notification reaches the screen first.
// 2.0.665 (John, 2026-09-20, seeing "pup: user-taskbar-menu needs this window on your screen" after a
// taskbar click): a HUMAN caller (the jump list's relaunch command routes here as pup_focus_window) asked
// for the window themselves. Warning them about their own click is noise, so no toast and no wait.
let human = crate::USER_CALLERS.contains(&caller.thread.as_str());
if !human {
let _ = ctx.ab.call("notify_user", json!({ "title": format!("pup: {thread} needs this window on your screen"), "body": reason, "focus": { "hwnd": h }, "level": "warning" }), caller, AB_T);
std::thread::sleep(Duration::from_millis(1200));
}
// macOS raises natively (ab cannot target another app's window by handle there); Windows goes through ab.
let applied = if ctx.host.raise_window(h) { true } else {
let r = ctx.ab.call("desktop_bring_to_front", json!({ "hwnd": h }), caller, AB_T);
if r.is_err() { let _ = ctx.ab.call("desktop_window_show", json!({ "hwnd": h }), caller, AB_T); }
r.is_ok()
};
ctx.sessions.with_mut(&s.id, |x| { x.foreground = true; x.raise_granted = true; x.user_active_at_ms = pup_core::now_ms(); }); touch(ctx, &s.id, Some(reason));
if let Some(x) = ctx.sessions.get(&s.id) { ctx.sessions.save_file(&x); }
ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "focused": applied, "applied": applied, "placement": "foreground", "windowEtiquette": "foreground", "reason": reason, "captioned": true, "toasted": !human, "_hint": "Raised onto the user's screen with a caption and a toast naming why. Send it back with pup_lower_os_window when the demo is over.", "_next": ["pup_lower_os_window", "pup_screenshot"] }))
}
/// A HUMAN caller (jump-list task, toolbar button) just clicked something that needs the window on
/// their screen: raise it with a short caption, no toast (they initiated it), and mark the session
/// user-active so the park doctrine leaves it alone. Node v2.0.293 osRaiseSessionWindow for USER_CALLERS.
pub fn raise_for_user(ctx: &Ctx, s: &pup_session::Session, caller: &Caller, caption: &str) -> bool {
let Some(h) = hwnd_of(ctx, s) else { return false };
if ctx.settings.get("fgCaption", "on") != "off" {
let ms: u64 = ctx.settings.get("fgCaptionMs", "3500").parse().unwrap_or(3500);
let _ = ctx.ab.call("desktop_caption", json!({ "text": caption, "duration": ms, "expiresInMs": ms, "position": "bottom", "size": "medium", "id": format!("pup-fg-{}", s.id) }), caller, AB_T);
}
let applied = if ctx.host.foreground() == h { true } else if ctx.host.raise_window(h) { true } else {
let r = ctx.ab.call("desktop_bring_to_front", json!({ "hwnd": h }), caller, AB_T);
if r.is_err() { let _ = ctx.ab.call("desktop_window_show", json!({ "hwnd": h }), caller, AB_T); }
r.is_ok()
};
ctx.sessions.with_mut(&s.id, |x| { x.foreground = true; x.raise_granted = true; x.user_active_at_ms = pup_core::now_ms(); });
if let Some(x) = ctx.sessions.get(&s.id) { ctx.sessions.save_file(&x); }
applied
}
pub fn lower(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {
let s = match resolve(ctx, caller, args) { Ok(s) => s, Err(e) => return e };
let Some(h) = hwnd_of(ctx, &s) else { return err("no_window_handle", "pup has no OS window handle for this window yet", None) };
let mut applied = ctx.host.send_to_bottom_no_activate(h);
if ctx.ab.call("desktop_set_window_state", json!({ "hwnd": h, "state": "bottom", "force": true }), caller, AB_T).is_ok() { applied = true; }
ctx.sessions.with_mut(&s.id, |x| x.foreground = false);
if let Some(x) = ctx.sessions.get(&s.id) { ctx.sessions.save_file(&x); }
ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "applied": applied, "placement": "background", "windowEtiquette": "background", "_hint": "Sent behind the user's windows without stealing focus; still fully drivable." }))
}
pub fn alert(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {
let s = match resolve(ctx, caller, args) { Ok(s) => s, Err(e) => return e };
let Some(h) = hwnd_of(ctx, &s) else { return err("no_window_handle", "pup has no OS window handle for this window yet", None) };
let stop = args.get("stop").and_then(|b| b.as_bool()).unwrap_or(false);
let mode = crate::activity::mode(ctx);
if stop {
// Node v1.8.45: FLASHW_STOP only stops the pulse; the Win11 tint needs the activate-and-restore.
let r = ctx.ab.call("desktop_flash_window", json!({ "hwnd": h, "stop": true }), caller, AB_T);
let cleared = crate::activity::clear_attention(ctx, h, caller);
let truly = cleared == "cleared" || cleared == "already-foreground";
return ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "alerted": false, "cleared": truly, "applied": r.is_ok(), "clearMethod": cleared, "_hint": if truly { "Flash truly cleared: activated the window off-screen to drop the Win11 attention tint, then handed focus back. No pop, no focus steal." } else { "FLASHW_STOP ran but the persistent tint needs activation and that step failed." } }));
}
if mode == "progress" {
// Node v2.0.304 (John: "i thought our new setting was for you to not do the orange flash anymore"):
// in progress mode the nudge is a short full-bar pulse, never orange.
let _ = ctx.ab.call("desktop_taskbar", json!({ "hwnd": h, "progress": { "state": "normal", "value": 1.0 } }), caller, AB_T);
if let Some(a) = ctx.arc() { std::thread::spawn(move || { std::thread::sleep(Duration::from_millis(2500)); let _ = a.ab.call("desktop_taskbar", json!({ "hwnd": h, "progress": { "state": "none" } }), &crate::identity::internal_caller(), AB_T); }); }
return ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "alerted": true, "cleared": false, "applied": true, "method": "progress-pulse", "_hint": "taskbarActivity is \"progress\", so the nudge was a short full progress pulse on the button (no orange). pup_configure {taskbarActivity:\"flash\"} brings the orange flash back." }));
}
if mode == "off" { return ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "alerted": false, "applied": false, "_hint": "taskbarActivity is \"off\": no taskbar signal is painted." })); }
let r = ctx.ab.call("desktop_flash_window", json!({ "hwnd": h, "mode": "count", "count": 4 }), caller, AB_T);
let applied = r.is_ok(); ctx.sessions.with_mut(&s.id, |x| x.flash_count += 1);
ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "alerted": applied, "cleared": false, "applied": applied, "abError": r.as_ref().err().map(|e| e.to_string()), "_hint": "Flashed the taskbar (finite 4-count) via ab, no focus steal. Pass {stop:true} to clear." }))
}
pub fn progress(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {
let s = match resolve(ctx, caller, args) { Ok(s) => s, Err(e) => return e };
let Some(h) = hwnd_of(ctx, &s) else { return err("no_window_handle", "pup has no OS window handle for this window yet", None) };
let mode = args.get("mode").and_then(|m| m.as_str()).unwrap_or("indeterminate");
let progress = match mode {
"off" | "none" => json!({ "state": "none" }),
"value" => json!({ "state": "normal", "value": args.get("value").and_then(|v| v.as_f64()).unwrap_or(0.0).clamp(0.0, 100.0) / 100.0 }),
"error" => json!({ "state": "error" }),
"paused" => json!({ "state": "paused" }),
_ => json!({ "state": "indeterminate" }),
};
let mut p = json!({ "hwnd": h, "progress": progress });
if let Some(note) = args.get("note").and_then(|n| n.as_str()) { p["overlay"] = json!({ "tooltip": format!("Adom Pup — {note}") }); }
let applied = ctx.ab.call("desktop_taskbar", p, caller, AB_T).is_ok();
touch(ctx, &s.id, args.get("note").and_then(|n| n.as_str()));
ok(json!({ "success": true, "output": json!({ "ok": true, "sessionId": s.id, "mode": mode, "painted": applied, "note": args.get("note").and_then(|n| n.as_str()) }).to_string(), "sessionId": s.id, "window": s.id, "aiThread": s.owner, "mode": mode, "applied": applied, "_hint": "Taskbar progress painted on this window's button. Clear it with mode:\"off\" when the long task ends." }))
}