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.
← Commit history
v2.0.668 (insiders): No more taskbar-identity residue: a window's AUMID is unregistered when it closes; a sweep a minute after boot (and pup_sweep_identities on demand) unregisters every thread, per-window and category identity no live window holds, deletes dead shortcuts in pup's Start Menu folder and stale tile/icon generations. arav-rog had 116 dead keys and 20 dead shortcuts (John, 2026-09-20).
7 files changed
+100−14
rust/Cargo.lock+12−12@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-pup-bridge"-version = "2.0.665"+version = "2.0.668" dependencies = [ "pup-browser", "pup-cdp",@@ -655,7 +655,7 @@ dependencies = [ [[package]] name = "pup-browser"-version = "2.0.665"+version = "2.0.668" dependencies = [ "pup-core", "pup-host",@@ -667,7 +667,7 @@ dependencies = [ [[package]] name = "pup-cdp"-version = "2.0.665"+version = "2.0.668" dependencies = [ "serde_json", "thiserror",@@ -677,7 +677,7 @@ dependencies = [ [[package]] name = "pup-core"-version = "2.0.665"+version = "2.0.668" dependencies = [ "serde", "serde_json",@@ -687,7 +687,7 @@ dependencies = [ [[package]] name = "pup-dashboard"-version = "2.0.665"+version = "2.0.668" dependencies = [ "pup-cdp", "pup-core",@@ -700,7 +700,7 @@ dependencies = [ [[package]] name = "pup-host"-version = "2.0.665"+version = "2.0.668" dependencies = [ "serde", "serde_json",@@ -709,14 +709,14 @@ dependencies = [ [[package]] name = "pup-host-mac"-version = "2.0.665"+version = "2.0.668" dependencies = [ "pup-host", ] [[package]] name = "pup-host-win"-version = "2.0.665"+version = "2.0.668" dependencies = [ "pup-core", "pup-host",@@ -726,7 +726,7 @@ dependencies = [ [[package]] name = "pup-identity"-version = "2.0.665"+version = "2.0.668" dependencies = [ "fontdue", "image",@@ -736,7 +736,7 @@ dependencies = [ [[package]] name = "pup-session"-version = "2.0.665"+version = "2.0.668" dependencies = [ "pup-core", "serde",@@ -746,7 +746,7 @@ dependencies = [ [[package]] name = "pup-vault"-version = "2.0.665"+version = "2.0.668" dependencies = [ "pup-core", "pup-host",@@ -757,7 +757,7 @@ dependencies = [ [[package]] name = "pup-verbs"-version = "2.0.665"+version = "2.0.668" dependencies = [ "base64", "png 0.17.16",
rust/Cargo.toml+1−1@@ -4,7 +4,7 @@ members = ["core", "host", "host-win", "host-mac", "cdp", "browser", "session", # One version for the whole bridge, in lockstep with src/BRIDGE_VERSION (tools/ship.sh bumps both). [workspace.package]-version = "2.0.665"+version = "2.0.668" edition = "2021" license = "MIT"
rust/bridge/src/main.rs+1@@ -118,6 +118,7 @@ fn main() { pup_verbs::highfps::start_loop(c.clone()); pup_verbs::extras::start_identity_audit(c.clone()); pup_verbs::extras::start_profile_janitor(c.clone());+ { let c2 = c.clone(); std::thread::spawn(move || { std::thread::sleep(std::time::Duration::from_secs(60)); pup_verbs::identity::sweep_dead_identities(&c2, "boot"); }); } if let Ok(v) = c.ab.call("bridge_info", json!({}), &Caller::default(), Duration::from_secs(4)) { *c.ad_version.lock().unwrap() = v.get("version").and_then(|x| x.as_str()).map(|s| s.to_string()); } }); }
rust/host-win/src/lib.rs+26@@ -143,6 +143,32 @@ mod imp { Some(shown) } }+ fn list_aumids(&self, prefix: &str) -> Vec<String> {+ extern "system" {+ fn RegOpenKeyExW(hkey: isize, sub: *const u16, opts: u32, sam: u32, out: *mut isize) -> i32;+ fn RegEnumKeyExW(hkey: isize, idx: u32, name: *mut u16, name_len: *mut u32, res: *const u32, cls: *mut u16, cls_len: *mut u32, ft: *mut c_void) -> i32;+ fn RegCloseKey(hkey: isize) -> i32;+ }+ const HKEY_CURRENT_USER: isize = 0x80000001u32 as i32 as isize;+ const KEY_READ: u32 = 0x20019;+ let sub: Vec<u16> = "Software\\Classes\\AppUserModelId".encode_utf16().chain(std::iter::once(0)).collect();+ let mut out = Vec::new();+ unsafe {+ let mut h: isize = 0;+ if RegOpenKeyExW(HKEY_CURRENT_USER, sub.as_ptr(), 0, KEY_READ, &mut h) != 0 { return out; }+ let mut i = 0u32;+ loop {+ let mut buf = [0u16; 512]; let mut len = buf.len() as u32;+ let r = RegEnumKeyExW(h, i, buf.as_mut_ptr(), &mut len, std::ptr::null(), std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::null_mut());+ if r != 0 { break; }+ let name = String::from_utf16_lossy(&buf[..len as usize]);+ if name.starts_with(prefix) { out.push(name); }+ i += 1;+ }+ RegCloseKey(h);+ }+ out+ } fn stop_flash(&self, hwnd: i64) -> bool { #[repr(C)] struct FLASHWINFO { cb_size: u32, hwnd: HWND, flags: u32, count: u32, timeout: u32 } extern "system" { fn FlashWindowEx(p: *const FLASHWINFO) -> i32; }
rust/host/src/lib.rs+4@@ -72,6 +72,10 @@ pub trait HostWindows: Send + Sync { /// the ms until the first show, None if it never showed within `timeout_ms`. A per-poll EnumWindows was /// 22 ms behind Chrome's first show (z-watch, 2.0.639); the compositor paints a frame in 16. fn park_on_first_show(&self, hwnd: i64, timeout_ms: u64) -> Option<u64> { let _ = (hwnd, timeout_ms); None }+ /// 2.0.666: the subkey names under HKCU\Software\Classes\AppUserModelId that start with `prefix`+ /// (pup's own identities), so a boot sweep can unregister the ones no live window holds. John found+ /// 116 dead thread identities on arav-rog on 2026-09-20 ("are you still leaving aumid icon residue?").+ fn list_aumids(&self, prefix: &str) -> Vec<String> { let _ = prefix; Vec::new() } /// Milliseconds since the last keyboard/mouse input (GetLastInputInfo); None where unknown. fn idle_ms(&self) -> Option<u64> { None } /// The primary monitor's work area (x, y, w, h), excluding the taskbar.
rust/verbs/src/identity.rs+53@@ -54,6 +54,59 @@ pub fn boot(ctx: &Ctx) { } } +/// 2.0.666: unregister every pup identity no live window holds, and drop tile/icon generations nothing+/// references. Until now only pup_uninstall ever called desktop_unregister_app_identity, so every thread+/// that ever opened a window left its taskbar identity registry key (and, with aumidShortcut on, a Start Menu+/// entry Windows lists as an App): 116 keys on arav-rog by 2026-09-20. Runs a minute after boot (after+/// recovery has re-listed the windows it brings back) and on every window close for that window's identity.+pub fn sweep_dead_identities(ctx: &Ctx, why: &str) -> Value {+ let live: std::collections::HashSet<String> = ctx.sessions.all().iter().filter_map(|s| s.app_id.clone()).collect();+ let mine = [format!("{}.thread.", id::APP_ID), format!("{}.cat.", id::APP_ID), format!("{}.w.", id::APP_ID)];+ let mut removed = Vec::new(); let mut failed = 0usize;+ for k in ctx.host.list_aumids(&format!("{}.", id::APP_ID)) {+ if live.contains(&k) || !mine.iter().any(|p| k.starts_with(p)) { continue; }+ match ctx.ab.call("desktop_unregister_app_identity", json!({ "appId": k, "anchorFolder": ANCHOR_FOLDER }), &internal_caller(), AB_T) { Ok(_) => removed.push(k), Err(_) => failed += 1 }+ }+ // tile / icon generations: keep the current one and any an active identity still points at+ let mut dirs = Vec::new();+ for (root, cur) in [("pup-thread-tiles", id::TILE_GEN), ("pup-icons", id::ICON_GEN)] {+ let Ok(rd) = std::fs::read_dir(ctx.host.temp_dir().join(root)) else { continue };+ for e in rd.flatten() {+ let name = e.file_name().to_string_lossy().to_string();+ if name == cur { continue; }+ let in_use = live.iter().any(|a| a.contains(&format!(".{name}")) || a.ends_with(&name));+ if in_use { continue; }+ if std::fs::remove_dir_all(e.path()).is_ok() { dirs.push(format!("{root}/{name}")); }+ }+ }+ // the anchor folder is pup's own: every .lnk in it that no live window's thread or id matches is dead+ // (ab's unregister only removes the anchor of the key it is given; keys removed by older builds left theirs)+ let anchor_on = ctx.settings.get("aumidShortcut", "off") == "on";+ let mut lnks = Vec::new();+ if let Ok(appdata) = std::env::var("APPDATA") {+ let folder = PathBuf::from(appdata).join("Microsoft").join("Windows").join("Start Menu").join("Programs").join(ANCHOR_FOLDER);+ let keep: std::collections::HashSet<String> = ctx.sessions.all().iter().flat_map(|s| vec![s.owner.clone().unwrap_or_default(), s.id.clone()]).collect();+ if let Ok(rd) = std::fs::read_dir(&folder) {+ for e in rd.flatten() {+ let name = e.file_name().to_string_lossy().to_string();+ let Some(stem) = name.strip_suffix(".lnk") else { continue };+ if keep.contains(stem) || (anchor_on && stem == id::DISPLAY_NAME) { continue; }+ if std::fs::remove_file(e.path()).is_ok() { lnks.push(stem.to_string()); }+ }+ if std::fs::read_dir(&folder).map(|mut r| r.next().is_none()).unwrap_or(false) { let _ = std::fs::remove_dir(&folder); }+ }+ }+ if !removed.is_empty() || !dirs.is_empty() || !lnks.is_empty() || failed > 0 { pup_core::log(format!("[identity] sweep ({why}): unregistered {} dead identit{} ({failed} failed), removed {} dead Start Menu shortcut(s), {} stale icon generation(s){}", removed.len(), if removed.len() == 1 { "y" } else { "ies" }, lnks.len(), dirs.len(), if dirs.is_empty() { String::new() } else { format!(": {}", dirs.join(", ")) })); }+ json!({ "unregistered": removed.len(), "failed": failed, "shortcutsRemoved": lnks, "iconDirsRemoved": dirs, "live": live.len(), "sample": removed.iter().take(10).collect::<Vec<_>>() })+}++/// Called after a window closed: its identity goes unless another live window shares it (category+/// AUMIDs are shared; thread AUMIDs are per thread and other windows of the thread hold the same one).+pub fn release_identity(ctx: &Ctx, app_id: &str) {+ if app_id == id::APP_ID || ctx.sessions.all().iter().any(|s| s.app_id.as_deref() == Some(app_id)) { return; }+ let _ = ctx.ab.call("desktop_unregister_app_identity", json!({ "appId": app_id, "anchorFolder": ANCHOR_FOLDER }), &internal_caller(), AB_T);+}+ pub fn mode(ctx: &Ctx) -> String { ctx.settings.all().get("taskbarMode").and_then(|v| v.as_str()).map(|s| s.to_string()).unwrap_or_else(|| if ctx.settings.get("aumidIcons", "on") != "on" { "plain".into() } else if ctx.settings.get("threadTiles", "on") == "on" { "tiles".into() } else { "aumid".into() }) }
rust/verbs/src/lib.rs+3−1@@ -535,7 +535,7 @@ pub const PHASE1_VERBS: &[&str] = &["pup_describe", "pup_readiness", "pup_prewar "pup_raise_os_window", "pup_focus_window", "pup_lower_os_window", "pup_alert_window", "pup_progress", "pup_configure", "pup_use", "pup_clear_cookies", "pup_clear_site_data", "pup_quit_idle_browsers", "pup_rescan", "pup_restart_browser", "pup_set_identity", "pup_audit_icons", "pup_dashboard", "pup_credentials", "credential_list", "credential_set", "credential_delete", "pup_login", "pup_google_accounts", "pup_google_signin", "pup_credential_reveal", "pup_card_set", "pup_cards", "pup_card_delete", "pup_fill_card", "pup_wiki_set_view", "pup_annotate", "pup_floaty_click", "pup_record_start", "pup_record_stop", "pup_record_status", "pup_record_list", "pup_recorder_open", "pup_recorder_close", "pup_screen_record_start", "pup_screen_record_stop", "pup_screen_record_status", "pup_screen_record_list", "pup_highfps_extend",- "pup_screenshot_element", "pup_screenshot_full_res", "pup_download", "pup_launch", "pup_deps", "pup_install_toolbar", "pup_reap_profiles", "pup_uninstall", "pup_move_to_workspace", "pup_assisted_click", "pup_import_scan", "pup_import_browser_logins", "pup_vault_sync", "pup_vault_export", "pup_vault_import"];+ "pup_screenshot_element", "pup_screenshot_full_res", "pup_download", "pup_launch", "pup_deps", "pup_install_toolbar", "pup_reap_profiles", "pup_sweep_identities", "pup_uninstall", "pup_move_to_workspace", "pup_assisted_click", "pup_import_scan", "pup_import_browser_logins", "pup_vault_sync", "pup_vault_export", "pup_vault_import"]; pub fn dispatch(ctx: &Ctx, caller: &Caller, verb: &str, args: &Value) -> Value { let t0 = Instant::now();@@ -635,6 +635,7 @@ pub fn dispatch(ctx: &Ctx, caller: &Caller, verb: &str, args: &Value) -> Value { "pup_deps" => extras::deps(ctx), "pup_install_toolbar" => extras::install_toolbar(ctx), "pup_reap_profiles" => extras::reap_profiles(ctx, args),+ "pup_sweep_identities" => { let r = identity::sweep_dead_identities(ctx, "verb"); ok(pup_core::merge(r, json!({ "_hint": "Unregistered every pup taskbar identity (its registry key and any Start Menu anchor) that no live window holds, and removed icon generations nothing references. pup runs this a minute after every boot and releases each window's identity on close (2.0.666)." }))) }, "pup_uninstall" => extras::uninstall(ctx, args), "pup_move_to_workspace" => extras::move_to_workspace(ctx, caller, args), "pup_assisted_click" => assist::assisted_click(ctx, caller, args),@@ -2013,6 +2014,7 @@ pub fn close_window(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { continue; } ctx.sessions.remove(&s.id); ctx.sessions.delete_file(&s.id); closed.push(s.id.clone()); // again: a late save loses+ if let Some(a) = s.app_id.as_deref() { identity::release_identity(ctx, a); } // 2.0.666: no dead AUMID left behind // last ext window: let the lane browser go, the same rule the CDP browsers follow below if transport::is_ext(&s) && !ctx.sessions.all().iter().any(|x| transport::is_ext(x) && x.profile == s.profile) { // 2.0.659: the LAST window of this profile's lane ends that lane's browser (the default lane's rule unchanged)