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.701 (insiders): Annotate starts in Multi mode: draw several shapes, press Enter to finish. Space toggles Multi and…
1c42ac3
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
//! The taskbar ACTIVITY signal (Node v2.0.289 to v2.0.304, `taskbarActivity` = progress | flash | off,
//! default progress). In `progress` mode pup NEVER paints orange: a determinate bar sized to the
//! running average of each operation (icon apply from open until the icon lands, screenshots), an
//! alert is a short full-bar pulse, and the Windows 11 attention tint a background-born window gets
//! is cleared through ab's `desktop_clear_window_attention` (FLASHW_STOP alone never clears it).
//! `flash` = the old finite four-count orange flash on open and on alert. `off` = nothing.
//! openFlash / autoFlash / agentActivity were removed in Node 2.0.292; the Rust port carried them
//! as dead keys until 2.0.425 (John, 2026-09-06: "you seemed to have strayed from that").
use crate::Ctx;
use pup_core::Caller;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
const DEFAULT_EST_MS: [(&str, u64); 4] = [("icon-apply", 2200), ("screenshot", 900), ("navigate", 2500), ("open", 3200)];
static TIMING: Mutex<Option<HashMap<String, (u64, u64)>>> = Mutex::new(None); // op -> (n, avgMs), persisted
pub fn mode(ctx: &Ctx) -> String { let m = ctx.settings.get("taskbarActivity", "progress"); if m == "flash" || m == "off" { m } else { "progress".into() } }
pub fn minimal_touch(ctx: &Ctx) -> bool { ctx.settings.get("minimalTouch", "on") == "on" }
fn timing_file(ctx: &Ctx) -> std::path::PathBuf { ctx.host.state_dir().join("pup-verb-timing.json") }
fn load(ctx: &Ctx) {
let mut g = TIMING.lock().unwrap();
if g.is_some() { return; }
let mut m = HashMap::new();
if let Ok(s) = std::fs::read_to_string(timing_file(ctx)) { if let Ok(v) = serde_json::from_str::<Value>(&s) { if let Some(o) = v.as_object() { for (k, e) in o { m.insert(k.clone(), (e["n"].as_u64().unwrap_or(0), e["avgMs"].as_u64().unwrap_or(0))); } } } }
*g = Some(m);
}
pub fn estimate_ms(ctx: &Ctx, op: &str) -> u64 {
load(ctx);
let g = TIMING.lock().unwrap();
g.as_ref().and_then(|m| m.get(op)).filter(|(n, a)| *n > 0 && *a > 0).map(|(_, a)| *a)
.unwrap_or_else(|| DEFAULT_EST_MS.iter().find(|(k, _)| *k == op).map(|(_, v)| *v).unwrap_or(1200))
}
fn record(ctx: &Ctx, op: &str, ms: u64) {
if ms > 120_000 { return; }
load(ctx);
let snapshot = {
let mut g = TIMING.lock().unwrap(); let m = g.as_mut().unwrap();
let e = m.entry(op.to_string()).or_insert((0, ms));
e.1 = if e.0 == 0 { ms } else { (e.1 * 3 + ms) / 4 }; // Node: avg*0.75 + ms*0.25, recent runs weigh more
e.0 = (e.0 + 1).min(100_000);
json!(m.iter().map(|(k, (n, a))| (k.clone(), json!({ "n": n, "avgMs": a }))).collect::<serde_json::Map<_, _>>())
};
let _ = std::fs::write(timing_file(ctx), snapshot.to_string());
}
fn push(ctx: &Ctx, hwnd: i64, value: Option<u64>) {
let p = match value { Some(v) => json!({ "state": "normal", "value": (v.min(100) as f64) / 100.0 }), None => json!({ "state": "none" }) };
let _ = ctx.ab.call("desktop_taskbar", json!({ "hwnd": hwnd, "progress": p }), &crate::identity::internal_caller(), Duration::from_secs(3));
}
/// Node withTaskbarProgress: a bar that fills over the running-average estimate for `op`, capped at
/// 92% until the work really finishes, then 100% and cleared. Records the true duration.
pub fn with_progress<T>(ctx: &Ctx, sid: &str, op: &str, work: impl FnOnce() -> T) -> T {
let Some(hwnd) = ctx.sessions.get(sid).and_then(|s| s.hwnd) else { return work() };
let r = progress_on_hwnd(ctx, hwnd, op, work);
ctx.sessions.with_mut(sid, |x| x.progress_count += 1);
r
}
/// The ONE progress-bar implementation: a bar that fills over the running-average estimate for `op`,
/// capped at 92% until the work finishes, then 100% and cleared. Both entry points use it — the
/// session-keyed `with_progress` and the extension lane (which has no session), so the lane no longer
/// carries a hand-rolled flat bar (John, 2026-09-13: "why ... you aren't sharing your own code").
pub fn progress_on_hwnd<T>(ctx: &Ctx, hwnd: i64, op: &str, work: impl FnOnce() -> T) -> T {
// 2.0.699: minimalTouch no longer gates the bar. It is on by default, so this line made the default
// "progress" activity paint nothing for anyone (John, 2026-10-05: "when do you animate the progress bar now?").
// taskbarActivity alone decides, as the Settings doc always said the per-feature settings do.
if !cfg!(windows) || mode(ctx) != "progress" { return work(); }
let Some(arc) = ctx.arc() else { return work() };
let est = estimate_ms(ctx, op).max(200); let started = std::time::Instant::now();
let done = Arc::new(std::sync::atomic::AtomicBool::new(false)); let d2 = done.clone();
push(ctx, hwnd, Some(2));
let arc2 = arc.clone();
std::thread::Builder::new().name("taskbar-progress".into()).spawn(move || {
while !d2.load(std::sync::atomic::Ordering::Relaxed) {
std::thread::sleep(Duration::from_millis(180));
if d2.load(std::sync::atomic::Ordering::Relaxed) { break; }
let f = (started.elapsed().as_millis() as f64 / est as f64).min(0.92);
push(&arc2, hwnd, Some(((f * 100.0) as u64).max(2)));
}
}).ok();
let r = work();
done.store(true, std::sync::atomic::Ordering::Relaxed);
record(ctx, op, started.elapsed().as_millis() as u64);
push(ctx, hwnd, Some(100));
std::thread::spawn(move || { std::thread::sleep(Duration::from_millis(220)); push(&arc, hwnd, None); });
r
}
/// Node startIconApplyProgress: from open (earliest) until the taskbar icon actually lands (latest),
/// timed to the running average of past icon applies; resolves the hwnd once it exists.
pub fn start_icon_apply(ctx: Arc<Ctx>, sid: String) {
if !cfg!(windows) || mode(&ctx) != "progress" { return; } // 2.0.699: not gated by minimalTouch (see progress_on_hwnd)
let est = estimate_ms(&ctx, "icon-apply").max(200);
std::thread::Builder::new().name("icon-apply-progress".into()).spawn(move || {
let started = std::time::Instant::now(); let mut hwnd: Option<i64> = None; let mut landed = false;
for _ in 0..40 {
let Some(s) = ctx.sessions.get(&sid) else { break };
if s.icon_applied { landed = true; break; }
if hwnd.is_none() { hwnd = s.hwnd; }
if let Some(h) = hwnd { let f = (started.elapsed().as_millis() as f64 / est as f64).min(0.92); push(&ctx, h, Some(((f * 100.0) as u64).max(2))); }
std::thread::sleep(Duration::from_millis(200));
}
if let Some(h) = hwnd { if landed { record(&ctx, "icon-apply", started.elapsed().as_millis() as u64); push(&ctx, h, Some(100)); std::thread::sleep(Duration::from_millis(220)); } push(&ctx, h, None); }
}).ok();
}
pub fn mark_icon_applied(ctx: &Ctx, sid: &str) { ctx.sessions.with_mut(sid, |x| x.icon_applied = true); }
/// Node osClearFlashByActivate (v1.8.45 / v2.0.27): the Windows 11 attention tint a background-born
/// window wears (the OS flashes a window it denied the foreground) clears only on activation; ab's
/// `desktop_clear_window_attention` does the off-screen activate-and-restore with net-zero focus.
/// Skipped when the window already IS the foreground (nothing to clear, and it would be a no-op).
pub fn clear_attention(ctx: &Ctx, hwnd: i64, caller: &Caller) -> String {
if !cfg!(windows) { return "not-windows".into(); }
if let Ok(fg) = ctx.ab.call("desktop_get_foreground", json!({}), caller, Duration::from_secs(4)) { if fg["hwnd"].as_i64() == Some(hwnd) { return "already-foreground".into(); } }
let r = match ctx.ab.call("desktop_clear_window_attention", json!({ "hwnd": hwnd }), caller, Duration::from_secs(8)) {
Ok(r) => if r["cleared"].as_bool() == Some(true) || r["ok"].as_bool() == Some(true) { "cleared".to_string() } else { "error".to_string() },
Err(e) => format!("error: {e}"),
};
// 2.0.585 (adom-bridge#199): ab's clear RAISES the window to z1 (measured: z16 -> z1 on every call), which undid
// every park at open and after every navigation. We only get here when the window is NOT the user's foreground
// (checked above), so putting it back at the bottom restores the resting state and can never take a window
// from the user. Remove once ab's verb stops touching placement.
// 2.0.588: re-check AFTER the clear (the user may have raised it during the round trip): a foreground window
// is the user's and is never touched.
if ctx.host.foreground() != hwnd { ctx.host.send_to_bottom_no_activate(hwnd); }
r
}
/// The birth signal: `flash` = finite four-count orange flash (never forever); `progress` = the
/// icon-apply bar (started by the caller) plus the attention-tint clear; `off` = only the clear.
/// 2.0.637: clear the attention tint the open/navigate flash leaves, WITHOUT ab. ab's clear verb raises and
/// briefly activates the window (adom-bridge#199): the z-watch saw every newborn become the FOREGROUND window
/// for 40-80 ms about a second after the jump list committed, which the foreground gate caught one run in three.
/// Let the pulses show, then stop the flash natively; a window the user has foregrounded is left alone.
pub fn clear_attention_later(ctx: &Ctx, hwnd: i64) {
if !cfg!(windows) { return; }
let Some(a) = ctx.arc() else { return };
std::thread::Builder::new().name("flash-stop".into()).spawn(move || {
std::thread::sleep(Duration::from_millis(2500));
if a.host.foreground() != hwnd { a.host.stop_flash(hwnd); }
}).ok();
}
pub fn on_open(ctx: &Ctx, sid: &str, hwnd: i64, caller: &Caller) {
if !cfg!(windows) { return; }
if mode(ctx) == "flash" && !minimal_touch(ctx) { let _ = ctx.ab.call("desktop_flash_window", json!({ "hwnd": hwnd, "mode": "count", "count": 4 }), caller, Duration::from_secs(6)); ctx.sessions.with_mut(sid, |x| x.flash_count += 1); }
}