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
Update 3 file(s)
3 files changed
+17−3
CHANGELOG.md+9@@ -1,5 +1,14 @@ # Changelog — Puppeteer Bridge +## 2.0.438 — 2026-09-06 — pup_open_window honours `owner`; every thread tile re-rendered++Found while recreating all of John's windows on AdomLapper as proof: pup_open_window ignored the declared+`owner` (Node: ownerSource "declared"), so windows recreated on another thread's behalf wore the caller's+tile. Fixed. The tile cache generation is bumped (t7 to t8) so every existing thread name is re-rendered+by the 2.0.437 multi-size renderer instead of reusing a cached single-size ICO. (The build container's+volume filled during this release; two source files were truncated by the failed writes and restored+from the wiki repo copies.)+ ## 2.0.437 — 2026-09-06 — badges that load under CSP, tiles rendered at every size, one button born with the tile Three of John's findings in one release:
rust/identity/src/lib.rs+2−2@@ -9,7 +9,7 @@ use std::path::{Path, PathBuf}; pub const APP_ID: &str = "Adom.Pup"; pub const DISPLAY_NAME: &str = "Adom Pup";-pub const TILE_GEN: &str = "t7";+pub const TILE_GEN: &str = "t8"; pub const ICON_GEN: &str = "9"; pub const TITLE_LABEL: &str = "ai-thread"; @@ -358,7 +358,7 @@ mod tests { // Node: threadSlug('pup-bridge') = 'pup-bridge-' + sha1('pup-bridge')[..6] assert_eq!(thread_slug("pup-bridge"), format!("pup-bridge-{}", &sha1_hex(b"pup-bridge")[..6])); assert!(thread_slug("My Thread / x").starts_with("my-thread-x-"));- assert_eq!(thread_aumid("t", "iabc123"), format!("Adom.Pup.thread.t-{}.t7iabc123", &sha1_hex(b"t")[..6]));+ assert_eq!(thread_aumid("t", "iabc123"), format!("Adom.Pup.thread.t-{}.{}iabc123", &sha1_hex(b"t")[..6], TILE_GEN)); } #[test] fn classify() { assert_eq!(classify_url("https://wiki.adom.inc/adom/x"), Some("wiki")); assert_eq!(classify_url("http://localhost:3000/"), Some("app")); assert_eq!(classify_url("https://x.adom.cloud/proxy/1/"), Some("app")); assert_eq!(classify_url("https://example.com"), Some("web")); assert_eq!(classify_url("about:blank"), None); } #[test] fn ico_wraps_png() { let png = [0x89, b'P', b'N', b'G', b'\r', b'\n', 0x1a, b'\n', 0,0,0,13, b'I',b'H',b'D',b'R', 0,0,1,0, 0,0,1,0, 8,6,0,0,0]; assert_eq!(png_size(&png), Some((256, 256))); let ico = png_to_ico(&png, 256, 256); assert_eq!(&ico[..4], &[0,0,1,0]); assert_eq!(ico.len(), png.len() + 22); }
rust/verbs/src/lib.rs+6−1@@ -668,7 +668,12 @@ fn open_window(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { let before: std::collections::HashSet<i64> = ctx.host.windows_for_pids(&[], Some("Chrome_WidgetWin_1")).into_iter().map(|w| w.hwnd).collect(); let prev_fg = ctx.host.foreground(); // Node v1.9.253: measure whether creation steals focus, and hand it straight back let target = match b.cdp.create_target("about:blank", true, Some((OFFSCREEN, OFFSCREEN, bw, bh))) { Ok(t) => t, Err(e) => return err("cdp_create_target_failed", format!("Target.createTarget: {e}"), Some("The browser is up but refused a new window; pup_readiness shows its state, pup_close {} restarts it.")) };- let mut s = Session::new(&sid, &profile, if caller.thread.is_empty() { None } else { Some(&caller.thread) }, Some(&caller.container), b.launched.cdp_port, if b.launched.pid > 0 { Some(b.launched.pid) } else { None });+ // Node: an explicit `owner` declares the owning thread (ownerSource "declared"); else the caller's thread.+ // The Rust port ignored `owner` until 2.0.438, so a window recreated on another thread's behalf wore the+ // recreating thread's tile (John's AdomLapper proof run, 2026-09-06).+ let declared_owner = args.get("owner").and_then(|o| o.as_str()).map(|o| o.trim().to_string()).filter(|o| !o.is_empty());+ let owner: Option<&str> = declared_owner.as_deref().or(if caller.thread.is_empty() { None } else { Some(caller.thread.as_str()) });+ let mut s = Session::new(&sid, &profile, owner, Some(&caller.container), b.launched.cdp_port, if b.launched.pid > 0 { Some(b.launched.pid) } else { None }); s.owner_from_caller = s.owner.as_deref() == Some(caller.thread.as_str()) && !caller.thread.is_empty() && args.get("owner").is_none(); let tab_id = match attach_tab(&mut s, &b.cdp, &target) { Ok(t) => t, Err(e) => return e }; // park: on-screen rect via CDP (moves without z-order change), then bottom + tag via the host