← 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).

John Lauer ·bf97c04fb9 ·1mo ago ·parent 8479888
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) {