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.718 (insiders): The owner chip is off on desktops that had the old default saved (a stored "on" from before 2.0.713 is turned off once; a later choice is kept). Restored windows keep their videos paused however they come back and never wait on it (2.0.717). Input reaches isolated and named-profile windows (2.0.714). The foreground and jump-list gates run beside other threads' windows without touching them. Gates: smoke all pass on arav-rog and ConfRoomROG; foreground pass, parity 55/56 and jump list 11/14 (baseline, Close ALL skipped in shared mode) on arav-rog; other threads' windows unchanged before and after.
7 files changed
+55−20
rust/Cargo.lock+12−12@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-pup-bridge"-version = "2.0.713"+version = "2.0.718" dependencies = [ "pup-browser", "pup-cdp",@@ -655,7 +655,7 @@ dependencies = [ [[package]] name = "pup-browser"-version = "2.0.713"+version = "2.0.718" dependencies = [ "pup-core", "pup-host",@@ -667,7 +667,7 @@ dependencies = [ [[package]] name = "pup-cdp"-version = "2.0.713"+version = "2.0.718" dependencies = [ "serde_json", "thiserror",@@ -677,7 +677,7 @@ dependencies = [ [[package]] name = "pup-core"-version = "2.0.713"+version = "2.0.718" dependencies = [ "serde", "serde_json",@@ -687,7 +687,7 @@ dependencies = [ [[package]] name = "pup-dashboard"-version = "2.0.713"+version = "2.0.718" dependencies = [ "pup-cdp", "pup-core",@@ -700,7 +700,7 @@ dependencies = [ [[package]] name = "pup-host"-version = "2.0.713"+version = "2.0.718" dependencies = [ "serde", "serde_json",@@ -709,14 +709,14 @@ dependencies = [ [[package]] name = "pup-host-mac"-version = "2.0.713"+version = "2.0.718" dependencies = [ "pup-host", ] [[package]] name = "pup-host-win"-version = "2.0.713"+version = "2.0.718" dependencies = [ "pup-core", "pup-host",@@ -726,7 +726,7 @@ dependencies = [ [[package]] name = "pup-identity"-version = "2.0.713"+version = "2.0.718" dependencies = [ "fontdue", "image",@@ -736,7 +736,7 @@ dependencies = [ [[package]] name = "pup-session"-version = "2.0.713"+version = "2.0.718" dependencies = [ "pup-core", "serde",@@ -746,7 +746,7 @@ dependencies = [ [[package]] name = "pup-vault"-version = "2.0.713"+version = "2.0.718" dependencies = [ "pup-core", "pup-host",@@ -757,7 +757,7 @@ dependencies = [ [[package]] name = "pup-verbs"-version = "2.0.713"+version = "2.0.718" 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.713"+version = "2.0.718" edition = "2021" license = "MIT"
rust/verbs/src/extdrive.rs+1@@ -196,6 +196,7 @@ pub fn reload_stale_worker(ctx: &Ctx) { toast(format!("pup is updating to {to}"), format!("Your {} pup window{} will close and come back in about a minute, on the same pages. Nothing has crashed.", ids.len(), if ids.len() == 1 { "" } else { "s" })); } for id in &ids { ctx.sessions.remove(id); }+ crate::recovery::mark_browser_restarted(&cur_profile()); // 2.0.717: re-adopted windows get the media guard too let n = crate::kill_extdrive(ctx); if n > 0 { *ASKED.lock().unwrap() = Some(want.clone()); } // 2.0.690: done only once the old browser really ended // 2.0.650: WAIT for the windows to be gone before relaunching. The kill is asynchronous; relaunch_from_file saw
rust/verbs/src/extras.rs+1@@ -282,6 +282,7 @@ pub fn move_to_workspace(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { /// process by hand and retyping 20 tab URLs out of a log. pub fn restart_browser(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { let profile = args.get("profile").and_then(|p| p.as_str()).unwrap_or("adom-you").to_string();+ crate::recovery::mark_browser_restarted(&profile); // 2.0.717: windows Chrome brings back itself get the media guard too let sessions = ctx.sessions.for_profile(&profile); if sessions.is_empty() && crate::browser_for(ctx, &profile).is_none() { return err("no_browser", format!("no browser is running for profile \"{profile}\""), Some("pup_status lists the live profiles."));
rust/verbs/src/input.rs+5@@ -27,6 +27,11 @@ pub fn resolve_tab(ctx: &Ctx, caller: &Caller, args: &Value) -> Result<(Session, let ext = crate::transport::is_ext(&s); let by_url = args.get("_tabUrl").and_then(|u| u.as_str()).and_then(|u| crate::session_for_tab_url(ctx, u)).filter(|(w, _)| w.id == s.id).map(|(_, t)| t); let t = args.get("tabId").and_then(|t| t.as_str()).map(|id| s.tab_by_id(id).cloned()).unwrap_or_else(|| by_url.or_else(|| s.active().cloned())).ok_or_else(|| err("no_tab", "the window has no tabs", None))?;+ // 2.0.714: talk to the window's OWN browser. Without this every input verb on an isolated or named-profile window+ // (click, type, keys, mouse) went down the MAIN browser's channel: typing "capacitor" into an isolated window came+ // back "selector not found" because it searched the other browser's page (found by the shared-box jump-list gate,+ // 2026-10-12). lane_for has done this since 2.0.659; resolve_tab never did.+ if ext { crate::extdrive::enter_profile(&s.profile); } let b = if ext { crate::extdrive::ext_handle() } else { browser_for(ctx, &s.profile).ok_or_else(|| err("browser_gone", "the browser behind this window is not connected", Some("pup_close_window then pup_open_window; recovery reconnects live Chromes at boot.")))? }; crate::focus_emulate(ctx, &s.id, Some(&t.tab_id)); // 2.0.458: this tab is the one pup is driving Ok((s, t, b))
rust/verbs/src/recovery.rs+29−5@@ -87,6 +87,25 @@ struct ProfileScope; impl ProfileScope { fn enter(profile: &str) -> ProfileScope { crate::extdrive::enter_profile(profile); ProfileScope } } impl Drop for ProfileScope { fn drop(&mut self) { crate::extdrive::leave_profile(); } } +/// 2.0.717: profiles whose browser pup restarted on purpose in the last 2 min. Chrome can bring such windows back+/// itself (its own session restore); pup then RE-ADOPTS them instead of relaunching, and the relaunch path was the only+/// one that paused their autoplaying media. A plain pup restart (the browser kept running) is not in this set, so a+/// video someone is watching is never paused by it.+static RESTARTED: std::sync::Mutex<Vec<(String, u64)>> = std::sync::Mutex::new(Vec::new());+pub fn mark_browser_restarted(profile: &str) { let now = pup_core::now_ms(); let mut m = RESTARTED.lock().unwrap(); m.retain(|(p, t)| p != profile && now.saturating_sub(*t) < 120_000); m.push((profile.to_string(), now)); }+fn browser_restarted_recently(profile: &str) -> bool { let now = pup_core::now_ms(); RESTARTED.lock().unwrap().iter().any(|(p, t)| p == profile && now.saturating_sub(*t) < 120_000) }+/// Pause autoplaying media in a restored window's tabs, in the background (see quietMedia in worker.js).+fn quiet_restored(id: &str, profile: &str, wid: i64) {+ let (id2, prof2) = (id.to_string(), profile.to_string());+ std::thread::spawn(move || {+ crate::extdrive::enter_profile(&prof2);+ match crate::extdrive::run(json!({ "action": "quietMedia", "windowId": wid, "ms": 30000, "timeoutMs": 20000 }), Duration::from_secs(24)) {+ Ok(q) => pup_core::log(format!("[recovery] \"{id2}\": restored tabs keep their media paused for 30 s ({} tab(s) guarded, {} paused at once)", q["guarded"], q["paused"])),+ Err(e) => pup_core::log(format!("[recovery] \"{id2}\": could not quiet the restored tabs: {}", e["error"].as_str().unwrap_or("?"))),+ }+ });+}+ fn readopt_ext(ctx: &Ctx, f: &Value) -> bool { if f["transport"].as_str() != Some("ext") { return false; } let _scope = ProfileScope::enter(f["profile"].as_str().unwrap_or("adom-you"));@@ -131,6 +150,7 @@ fn readopt_ext(ctx: &Ctx, f: &Value) -> bool { if let Some(a) = ctx.arc() { crate::identity::schedule(a, s.id.clone(), crate::identity::internal_caller(), true); pup_core::log(format!("[recovery] \"{id}\": its tile is from an older drawing; re-stamping it")); } } pup_core::log(format!("[recovery] \"{id}\": re-adopted its extension-lane window (Chrome window {wid}, {} tab(s), hwnd {:?}); nothing relaunched", s.tabs.len(), s.hwnd));+ if browser_restarted_recently(&s.profile) { quiet_restored(&id, &s.profile, wid); } crate::extdrive::rebind_unbound(ctx); // stamp the re-adopted window NOW; the health tick is only a backstop true }@@ -248,11 +268,7 @@ pub fn relaunch_from_file(ctx: &Ctx, f: &Value) { // tabs for 30 s; anything the user starts by clicking plays normally. if r["ok"] == json!(true) { if let Some(wid) = ctx.sessions.get(&id).filter(|s| crate::transport::is_ext(s)).and_then(|s| s.window_id) {- let _scope = ProfileScope::enter(&profile);- match crate::extdrive::run(json!({ "action": "quietMedia", "windowId": wid, "ms": 30000, "timeoutMs": 40000 }), Duration::from_secs(45)) {- Ok(q) => pup_core::log(format!("[recovery] \"{id}\": restored tabs keep their media paused for 30 s ({} tab(s) guarded, {} paused at once)", q["guarded"], q["paused"])),- Err(e) => pup_core::log(format!("[recovery] \"{id}\": could not quiet the restored tabs: {}", e["error"].as_str().unwrap_or("?"))),- }+ quiet_restored(&id, &profile, wid); // 2.0.715: in the background; the next window's restore never waits on it } } // 2.0.471: a relaunch that WORKED resets the counter. It counted every relaunch in the bridge's life, so a window@@ -333,6 +349,14 @@ pub fn adopt_window_tabs(ctx: &Ctx, h: &Arc<BrowserHandle>) -> usize { } pub fn recover_at_boot(ctx: &Ctx) {+ // 2.0.718: the owner chip went default-OFF in 2.0.713 (John: it covers the page's own controls), but a desktop where+ // the Settings dialog had ever saved its values kept the old default stored as "on" (ConfRoomROG, measured). Before+ // 2.0.713 "on" was the default, so a stored "on" is almost never a choice: turn it off once. Anyone who turns the+ // chip on after this keeps it (the marker means the migration never runs again).+ if ctx.settings.get("ownerChipMigrated", "") != "713" {+ if ctx.settings.get("ownerChip", "off") == "on" { let _ = ctx.settings.set("ownerChip", json!("off")); pup_core::log("[settings] ownerChip was stored as the old default \"on\"; turned off once (2.0.713 made it default-off)"); }+ let _ = ctx.settings.set("ownerChipMigrated", json!("713"));+ } // 2.0.659 (pup-bridge#113): throwaway lanes do not survive a pup restart. End any browser still on an // iso-* profile and remove the profile; their windows were fleeting by definition. if let Ok(rd) = std::fs::read_dir(pup_browser::profiles_dir(&*ctx.host)) {
src/extension/worker.js+6−2@@ -445,10 +445,14 @@ async function pupExtPollOnce() { const iv = setInterval(() => { if (Date.now() > stopAt || user()) { clearInterval(iv); return; } pauseAll(); }, 500); return pauseAll(); };+ // 2.0.715: every tab is capped at ~15 s (12 s for the page, 3 s to inject). A tab whose page or injection never settles used to hold this command for+ // its full 40 s, and restores run one window after another, so one stuck tab delayed every window behind it.+ const capped = (p, ms2) => Promise.race([p, new Promise((res) => setTimeout(() => res('timeout'), ms2))]); const results = await Promise.all(tabs.map(async (t) => { try {- if (t.status !== 'complete') await pupExtWaitComplete(t.id, 15000);- const r = await chrome.scripting.executeScript({ target: { tabId: t.id, allFrames: true }, func: guard, args: [ms] });+ if (t.status !== 'complete') await capped(pupExtWaitComplete(t.id, 12000), 12200); // a guard injected before the page commits is thrown away with it+ const r = await capped(chrome.scripting.executeScript({ target: { tabId: t.id, allFrames: true }, func: guard, args: [ms] }), 3000);+ if (r === 'timeout') return -1; return (r || []).reduce((a, x) => a + (Number(x && x.result) || 0), 0); } catch (e) { return -1; } }));