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
CHANGELOG.md+9@@ -1,5 +1,14 @@ # Changelog — Puppeteer Bridge +## 2.0.443 — 2026-09-06 — no window left behind: closes verified against the OS, orphan windows swept on connect++Recreating John's windows across reinstalls left three Chrome windows nobody tracked, wearing old tiles:+a close that raced a bridge respawn had no live CDP handle, dropped the session and left the window.+pup_close_window now waits for the OS window to go and, if it survives, closes it through ab (WM_CLOSE,+forced after 3 s). And the connect-time sweep now closes any WINDOW no session owns (judged by window,+so a user's extra tab in a live pup window is never touched), not only about:blank leftovers.+Also the tile text gets the width the rounded plate allows (0.90 of the tile from 7.5% in).+ ## 2.0.442 — 2026-09-06 — thread tiles through the brand raster pipeline, in Rust, on the Adom rounded plate John: rounded corners like the Adom favicon, two lines at most, no ellipsis, and the downscaling that the
rust/verbs/src/lib.rs+17−6@@ -433,17 +433,24 @@ pub fn touch(ctx: &Ctx, sid: &str, note: Option<&str>) { ctx.sessions.with_mut(s /// published, so no verb can be creating a target in the gap. Popups (openerId) and every target a /// session owns are never touched. fn close_orphan_blank_targets(ctx: &Ctx, cdp: &Cdp, profile: &str) {+ // Orphans: (1) the about:blank launch window an older build left behind, and (2) whole WINDOWS no+ // session owns any more, left by a close that raced a bridge respawn (John's laptop, 2026-09-06:+ // three leftover windows wearing old tiles). A window is orphaned only when NONE of its tabs belongs+ // to a session, so a user's extra tab in a live pup window is never touched; popups never are. let Ok(targets) = cdp.targets() else { return }; let owned: std::collections::HashSet<String> = ctx.sessions.all().iter().flat_map(|s| s.tabs.iter().map(|t| t.target_id.clone())).collect();+ let pages: Vec<&Value> = targets.iter().filter(|t| t["type"] == json!("page") && !t.get("openerId").and_then(|o| o.as_str()).map(|o| !o.is_empty()).unwrap_or(false)).collect();+ let mut win_of: HashMap<String, i64> = HashMap::new();+ for t in &pages { if let Some(tid) = t["targetId"].as_str() { if let Ok((wid, _)) = cdp.window_for_target(tid) { win_of.insert(tid.to_string(), wid); } } }+ let owned_windows: std::collections::HashSet<i64> = win_of.iter().filter(|(tid, _)| owned.contains(*tid)).map(|(_, w)| *w).collect(); let mut closed = 0;- for t in targets {- let tid = t["targetId"].as_str().unwrap_or("");- let is_page = t["type"] == json!("page");+ for t in &pages {+ let tid = t["targetId"].as_str().unwrap_or(""); if tid.is_empty() || owned.contains(tid) { continue; } let blank = t["url"].as_str().map(|u| u == "about:blank" || u.is_empty()).unwrap_or(false);- let popup = t.get("openerId").and_then(|o| o.as_str()).map(|o| !o.is_empty()).unwrap_or(false);- if is_page && blank && !popup && !tid.is_empty() && !owned.contains(tid) { if cdp.close_target(tid).is_ok() { closed += 1; } }+ let orphan_window = win_of.get(tid).map(|w| !owned_windows.contains(w)).unwrap_or(false);+ if blank || orphan_window { if cdp.close_target(tid).is_ok() { closed += 1; pup_core::log(format!("[launch] closed orphan {} in profile \"{profile}\": {}", if blank { "about:blank window" } else { "window no session owns" }, t["url"].as_str().unwrap_or("").chars().take(60).collect::<String>())); } } }- if closed > 0 { pup_core::log(format!("[launch] closed {closed} orphan about:blank window(s) in profile \"{profile}\" (untracked launch window, stock taskbar icon)")); }+ if closed > 0 { pup_core::log(format!("[launch] {closed} orphan window(s) closed in profile \"{profile}\"")); } } pub fn browser_for(ctx: &Ctx, profile: &str) -> Option<Arc<BrowserHandle>> { ctx.browsers.lock().unwrap().get(profile).cloned().filter(|b| b.cdp.is_alive()) }@@ -884,6 +891,10 @@ pub fn close_window(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { let mut closed = Vec::new(); let mut chrome_killed = 0; for s in targets { if let Some(b) = browser_for(ctx, &s.profile) { for t in &s.tabs { let _ = b.cdp.close_target(&t.target_id); } }+ // Verify the OS window is gone; a close that races a bridge respawn (no live CDP handle yet) used to+ // drop the session and leave the window behind (John's laptop, 2026-09-06: three leftovers wearing+ // old tiles). The window is closed through ab (WM_CLOSE, forced after 3 s) when it survives.+ if let Some(h) = s.hwnd { std::thread::sleep(Duration::from_millis(300)); if ctx.host.is_alive(h) { let _ = ctx.ab.call("desktop_close_window", json!({ "hwnd": h, "forceAfterMs": 3000 }), caller, Duration::from_secs(6)); pup_core::log(format!("[close] \"{}\" window {h} survived the CDP close; closed through the OS", s.id)); } } ctx.sessions.remove(&s.id); ctx.sessions.delete_file(&s.id); closed.push(s.id.clone()); // last window on that profile: let the browser go (Node: killProfileBrowserIfUnused) if ctx.sessions.for_profile(&s.profile).is_empty() {