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.709 (insiders): Alt+Shift+A starts Annotate in pup windows (the page catches it; Chrome keeps the key for itself and never bound the extension's shortcut). Closing an isolated window ends its private browser and deletes its throwaway profile within about 30 s: pup now reads window closes from every browser, and its lookup of a profile's browser processes matches the profile folder correctly (it searched a path with doubled backslashes and found nothing). No more leftover Chrome for Testing icons in the system tray (John, 2026-10-11). Gates on arav-rog: smoke all pass, foreground pass, parity 55/56, jump list 12/15 (baseline).
8 files changed
+74−23
rust/Cargo.lock+12−12@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-pup-bridge"-version = "2.0.705"+version = "2.0.709" dependencies = [ "pup-browser", "pup-cdp",@@ -655,7 +655,7 @@ dependencies = [ [[package]] name = "pup-browser"-version = "2.0.705"+version = "2.0.709" dependencies = [ "pup-core", "pup-host",@@ -667,7 +667,7 @@ dependencies = [ [[package]] name = "pup-cdp"-version = "2.0.705"+version = "2.0.709" dependencies = [ "serde_json", "thiserror",@@ -677,7 +677,7 @@ dependencies = [ [[package]] name = "pup-core"-version = "2.0.705"+version = "2.0.709" dependencies = [ "serde", "serde_json",@@ -687,7 +687,7 @@ dependencies = [ [[package]] name = "pup-dashboard"-version = "2.0.705"+version = "2.0.709" dependencies = [ "pup-cdp", "pup-core",@@ -700,7 +700,7 @@ dependencies = [ [[package]] name = "pup-host"-version = "2.0.705"+version = "2.0.709" dependencies = [ "serde", "serde_json",@@ -709,14 +709,14 @@ dependencies = [ [[package]] name = "pup-host-mac"-version = "2.0.705"+version = "2.0.709" dependencies = [ "pup-host", ] [[package]] name = "pup-host-win"-version = "2.0.705"+version = "2.0.709" dependencies = [ "pup-core", "pup-host",@@ -726,7 +726,7 @@ dependencies = [ [[package]] name = "pup-identity"-version = "2.0.705"+version = "2.0.709" dependencies = [ "fontdue", "image",@@ -736,7 +736,7 @@ dependencies = [ [[package]] name = "pup-session"-version = "2.0.705"+version = "2.0.709" dependencies = [ "pup-core", "serde",@@ -746,7 +746,7 @@ dependencies = [ [[package]] name = "pup-vault"-version = "2.0.705"+version = "2.0.709" dependencies = [ "pup-core", "pup-host",@@ -757,7 +757,7 @@ dependencies = [ [[package]] name = "pup-verbs"-version = "2.0.705"+version = "2.0.709" 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.705"+version = "2.0.709" edition = "2021" license = "MIT"
rust/verbs/assets/floaty.js+1−1@@ -99,7 +99,7 @@ // already up, the hotkey switches its mode instead of being ignored. if (e.altKey && e.shiftKey && !e.ctrlKey && !e.metaKey && (e.key === 'A' || e.key === 'a' || e.key === 'M' || e.key === 'm')) { e.preventDefault(); e.stopPropagation();- var mm = (e.key === 'M' || e.key === 'm') ? 'multi' : 'auto';+ var mm = (e.key === 'M' || e.key === 'm') ? 'multi' : null; // 2.0.707: A opens in the default mode (Multi), like every other entry point if (window.__pupStartAnnotate) { window.__pupStartAnnotate('pen', mm); } window.__pupFloatyClick = Date.now(); if (typeof window.__pupFloatyPush === 'function') { try { window.__pupFloatyPush('hotkey'); window.__pupFloatyClick = null; } catch (e) {} } }
rust/verbs/src/lib.rs+7−2@@ -236,8 +236,13 @@ fn schedule_birth_reassert(ctx: &Ctx, sid: &str, why: &'static str) { /// host's shell runner). Used only when the lane is NOT alive, so every match is an orphan. pub fn orphan_browser_pids(ctx: &Ctx, profile_dir: &std::path::Path) -> Vec<u32> { if !cfg!(windows) { return Vec::new(); }- let needle = profile_dir.to_string_lossy().replace('\\', "\\\\").replace('\'', "''");- let script = format!("Get-CimInstance Win32_Process | Where-Object {{ $_.Name -eq 'chrome.exe' -and $_.CommandLine -and $_.CommandLine.Contains('{needle}') }} | ForEach-Object {{ $_.ProcessId }}");+ // 2.0.708: match the profile FOLDER NAME after pup-profiles, either slash. This doubled every backslash for a+ // PowerShell single-quoted string, where a backslash is not an escape, so it searched for "C:\\Users\\..." and never+ // matched: every boot and idle sweep of isolated profiles reported "ended 0 browser process(es)" and left the+ // browser running in the system tray (John's laptop, 2026-10-11).+ let name: String = profile_dir.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default().chars().filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_' || *c == '.').collect();+ if name.is_empty() { return Vec::new(); }+ let script = format!("Get-CimInstance Win32_Process | Where-Object {{ $_.Name -eq 'chrome.exe' -and $_.CommandLine -and $_.CommandLine -match 'pup-profiles[\\\\/]{name}(\\\"|\\s|$)' }} | ForEach-Object {{ $_.ProcessId }}"); let out = ctx.host.run_shell_script(&script, Duration::from_secs(20)); let mut v: Vec<u32> = out.lines().filter_map(|l| l.trim().parse().ok()).collect(); v.retain(|p| *p != std::process::id());
rust/verbs/src/recovery.rs+40−2@@ -510,14 +510,22 @@ fn forget_session(ctx: &Ctx, s: &Session, why: &str) { /// on its next poll, and a session whose OS window is dead while its lane is alive has no window to come back /// to. A dead hwnd whose Chrome window still exists is a stale handle, not a close: it is unbound for rebind. fn forget_user_closed(ctx: &Ctx) {+ // 2.0.707: once per browser. Each profile's browser reports its own closes on its own lane, and this only ever read+ // the default lane, so a closed ISOLATED window was never forgotten: its session stayed, sweep_idle_isolated (rightly)+ // kept its browser, and the windowless browser sat in the system tray (John, 2026-10-11).+ let mut profiles = vec!["adom-you".to_string()];+ for l in crate::extdrive::secondary_lanes() { if !profiles.contains(&l.profile) { profiles.push(l.profile.clone()); } }+ for p in profiles { let _scope = ProfileScope::enter(&p); forget_user_closed_on(ctx, &p); }+}+fn forget_user_closed_on(ctx: &Ctx, profile: &str) { let removed = crate::extdrive::take_removed();- let ext: Vec<Session> = ctx.sessions.all().into_iter().filter(|s| crate::transport::is_ext(s)).collect();+ let ext: Vec<Session> = ctx.sessions.all().into_iter().filter(|s| crate::transport::is_ext(s) && s.profile == profile).collect(); if ext.is_empty() { return; } for s in &ext { if let Some(w) = s.window_id { if removed.contains(&w) { forget_session(ctx, s, "its window was closed by the user (reported by the extension)"); } } } if !crate::extdrive::alive(ctx) || crate::extdrive::connected_ms_ago() > 8000 { return; }- let dead: Vec<Session> = ctx.sessions.all().into_iter().filter(|s| crate::transport::is_ext(s) && s.hwnd.map(|h| !ctx.host.is_alive(h)).unwrap_or(false)).collect();+ let dead: Vec<Session> = ctx.sessions.all().into_iter().filter(|s| crate::transport::is_ext(s) && s.profile == profile && s.hwnd.map(|h| !ctx.host.is_alive(h)).unwrap_or(false)).collect(); if dead.is_empty() { return; } let Ok(r) = crate::extdrive::run(json!({ "action": "tabs" }), Duration::from_secs(10)) else { return }; let live: Vec<i64> = r["tabs"].as_array().cloned().unwrap_or_default().iter().filter_map(|t| t["windowId"].as_i64()).collect();@@ -580,7 +588,37 @@ fn lane_gone_pass(ctx: &Ctx) { crate::kill_extdrive(ctx); } +/// 2.0.706: end isolated (iso-*) browsers whose windows are gone, and delete their throwaway profiles. This only+/// happened at boot, so every isolated window a thread opened and closed left its browser running for the rest of+/// the session, each one a Chrome for Testing icon in the system tray (John's laptop, 2026-10-07: three icons,+/// two of them windowless isolated browsers from 15:13 and 17:52). A profile no session uses on two health ticks in+/// a row goes; never while a window is being born.+pub fn sweep_idle_isolated(ctx: &Ctx) -> usize {+ static IDLE: std::sync::Mutex<Option<std::collections::HashSet<String>>> = std::sync::Mutex::new(None);+ let Ok(_birth) = crate::BIRTH_LOCK.try_lock() else { return 0 };+ let Ok(rd) = std::fs::read_dir(pup_browser::profiles_dir(&*ctx.host)) else { return 0 };+ let mut idle = IDLE.lock().unwrap();+ let prev = idle.take().unwrap_or_default();+ let mut now_idle = std::collections::HashSet::new();+ let mut n = 0;+ for e in rd.flatten() {+ let name = e.file_name().to_string_lossy().to_string();+ if !name.starts_with("iso-") || !ctx.sessions.for_profile(&name).is_empty() { continue; }+ if !prev.contains(&name) { now_idle.insert(name); continue; } // first sighting: wait one more tick+ let pids = crate::orphan_browser_pids(ctx, &e.path());+ for p in &pids { ctx.host.kill(*p); }+ std::thread::sleep(Duration::from_millis(if pids.is_empty() { 0 } else { 800 }));+ let removed = std::fs::remove_dir_all(e.path()).is_ok();+ crate::extdrive::drop_lane_for_profile(&name);+ pup_core::log(format!("[profile] isolated profile {name} has no window: ended {} browser process(es), removed={removed}", pids.len()));+ n += 1;+ }+ *idle = Some(now_idle);+ n+}+ pub fn health_tick(ctx: &Ctx) {+ sweep_idle_isolated(ctx); // 2.0.706: an isolated window's browser ends with its window crate::extdrive::adopt_tokenless(ctx); // 2.0.580: a token-less extension browser after a restart forget_user_closed(ctx); // 2.0.595: the user's close is final lane_gone_pass(ctx); // 2.0.595: a gone lane is forgotten unless it provably crashed
src/extension/content.js+11@@ -9,6 +9,17 @@ try { let cfg = null; try { cfg = await chrome.runtime.sendMessage({ t: 'pup-floaty-cfg' }); } catch (e) {}+ // 2.0.707: Alt+Shift+A from the page. Chrome keeps Alt+Shift+A for itself ("focus inactive dialogs") and never+ // bound the extension's pup-snip shortcut to it, so the hotkey has never worked in pup's own windows (John,+ // 2026-10-11). A page sees the key before Chrome's accelerator, so catch it here and start Annotate the same way+ // the corner pencil does. Independent of the pencil (off by default); pup's tbSnipHotkey setting turns it off.+ if (cfg && cfg.hotkey) {+ addEventListener('keydown', (e) => {+ if (!(e.altKey && e.shiftKey) || e.ctrlKey || e.metaKey || (e.key !== 'A' && e.key !== 'a' && e.code !== 'KeyA')) return;+ e.preventDefault(); e.stopPropagation();+ chrome.runtime.sendMessage({ t: 'pup-annotate', tool: 'pen' }).catch(() => {});+ }, true);+ } if (!cfg || !cfg.enabled) return; const host = document.createElement('div');
src/extension/manifest.json+1−4@@ -33,10 +33,7 @@ ], "commands": { "pup-snip": {- "suggested_key": {- "default": "Alt+Shift+A"- },- "description": "Annotate this page (red pen)"+ "description": "Annotate this page (Alt+Shift+A works inside pages without setting a key here)" }, "pup-view": { "suggested_key": {
src/extension/worker.js+1−1@@ -112,7 +112,7 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { if (msg && msg.t === 'pup-floaty-cfg') { const found = await findPort(); const tb = (found && found.health && found.health.toolbar) || {};- sendResponse({ enabled: !!found && tb.enabled !== false && tb.floaty !== false });+ sendResponse({ enabled: !!found && tb.enabled !== false && tb.floaty !== false, hotkey: !!found && tb.enabled !== false && tb.snipHotkey !== false }); return; } if (msg && msg.t === 'pup-cred' && msg.rec) {