← Commit history

v2.0.701 (insiders): Annotate starts in Multi mode: draw several shapes, press Enter to finish. Space toggles Multi and Single (the old Auto), M and S pick one, Esc cancels, and a hint under the toolbar shows the keys (John, 2026-10-06). Also: the taskbar progress bar on every page load, on the window being worked on (needs Adom Bridge 2.1.169); restored windows keep their videos paused (YouTube plays about 3 s first); notices when an update restarts pup's browser. Gates on winvm: smoke all pass, foreground pass, parity 55/56, jump list 11/14 (the user-raise check skipped by Windows' foreground lock).

John Lauer ·1c42ac392a ·15d ago ·parent 9870353
8 files changed +108−36
rust/Cargo.lock+12−12
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-pup-bridge"-version = "2.0.697"+version = "2.0.701" dependencies = [  "pup-browser",  "pup-cdp",@@ -655,7 +655,7 @@ dependencies = [  [[package]] name = "pup-browser"-version = "2.0.697"+version = "2.0.701" dependencies = [  "pup-core",  "pup-host",@@ -667,7 +667,7 @@ dependencies = [  [[package]] name = "pup-cdp"-version = "2.0.697"+version = "2.0.701" dependencies = [  "serde_json",  "thiserror",@@ -677,7 +677,7 @@ dependencies = [  [[package]] name = "pup-core"-version = "2.0.697"+version = "2.0.701" dependencies = [  "serde",  "serde_json",@@ -687,7 +687,7 @@ dependencies = [  [[package]] name = "pup-dashboard"-version = "2.0.697"+version = "2.0.701" dependencies = [  "pup-cdp",  "pup-core",@@ -700,7 +700,7 @@ dependencies = [  [[package]] name = "pup-host"-version = "2.0.697"+version = "2.0.701" dependencies = [  "serde",  "serde_json",@@ -709,14 +709,14 @@ dependencies = [  [[package]] name = "pup-host-mac"-version = "2.0.697"+version = "2.0.701" dependencies = [  "pup-host", ]  [[package]] name = "pup-host-win"-version = "2.0.697"+version = "2.0.701" dependencies = [  "pup-core",  "pup-host",@@ -726,7 +726,7 @@ dependencies = [  [[package]] name = "pup-identity"-version = "2.0.697"+version = "2.0.701" dependencies = [  "fontdue",  "image",@@ -736,7 +736,7 @@ dependencies = [  [[package]] name = "pup-session"-version = "2.0.697"+version = "2.0.701" dependencies = [  "pup-core",  "serde",@@ -746,7 +746,7 @@ dependencies = [  [[package]] name = "pup-vault"-version = "2.0.697"+version = "2.0.701" dependencies = [  "pup-core",  "pup-host",@@ -757,7 +757,7 @@ dependencies = [  [[package]] name = "pup-verbs"-version = "2.0.697"+version = "2.0.701" 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.697"+version = "2.0.701" edition = "2021" license = "MIT" 
rust/verbs/src/activity.rs+5−2
@@ -66,7 +66,10 @@ pub fn with_progress<T>(ctx: &Ctx, sid: &str, op: &str, work: impl FnOnce() -> T /// session-keyed `with_progress` and the extension lane (which has no session), so the lane no longer /// carries a hand-rolled flat bar (John, 2026-09-13: "why ... you aren't sharing your own code"). pub fn progress_on_hwnd<T>(ctx: &Ctx, hwnd: i64, op: &str, work: impl FnOnce() -> T) -> T {-    if !cfg!(windows) || mode(ctx) != "progress" || minimal_touch(ctx) { return work(); }+    // 2.0.699: minimalTouch no longer gates the bar. It is on by default, so this line made the default+    // "progress" activity paint nothing for anyone (John, 2026-10-05: "when do you animate the progress bar now?").+    // taskbarActivity alone decides, as the Settings doc always said the per-feature settings do.+    if !cfg!(windows) || mode(ctx) != "progress" { return work(); }     let Some(arc) = ctx.arc() else { return work() };     let est = estimate_ms(ctx, op).max(200); let started = std::time::Instant::now();     let done = Arc::new(std::sync::atomic::AtomicBool::new(false)); let d2 = done.clone();@@ -91,7 +94,7 @@ pub fn progress_on_hwnd<T>(ctx: &Ctx, hwnd: i64, op: &str, work: impl FnOnce() - /// Node startIconApplyProgress: from open (earliest) until the taskbar icon actually lands (latest), /// timed to the running average of past icon applies; resolves the hwnd once it exists. pub fn start_icon_apply(ctx: Arc<Ctx>, sid: String) {-    if !cfg!(windows) || mode(&ctx) != "progress" || minimal_touch(&ctx) { return; }+    if !cfg!(windows) || mode(&ctx) != "progress" { return; }   // 2.0.699: not gated by minimalTouch (see progress_on_hwnd)     let est = estimate_ms(&ctx, "icon-apply").max(200);     std::thread::Builder::new().name("icon-apply-progress".into()).spawn(move || {         let started = std::time::Instant::now(); let mut hwnd: Option<i64> = None; let mut landed = false;
rust/verbs/src/extdrive.rs+15−11
@@ -207,10 +207,13 @@ pub fn reload_stale_worker(ctx: &Ctx) {     for f in files { crate::recovery::relaunch_from_file(ctx, &f); }     if !ids.is_empty() {         let back: Vec<&String> = ids.iter().filter(|id| ctx.sessions.exists(id)).collect();-        let missing: Vec<&str> = ids.iter().filter(|id| !ctx.sessions.exists(id)).map(|s| s.as_str()).collect();+        // 2.0.698: a window the user closed while it was being restored is final, not "still coming" (John closed two)+        let closed: Vec<&str> = ids.iter().filter(|id| !ctx.sessions.exists(id) && crate::recovery::restore_cancelled(id)).map(|s| s.as_str()).collect();+        let missing: Vec<&str> = ids.iter().filter(|id| !ctx.sessions.exists(id) && !crate::recovery::restore_cancelled(id)).map(|s| s.as_str()).collect();         let secs = t0.elapsed().as_secs();-        if missing.is_empty() { toast(format!("pup updated to {to}"), format!("All {} window{} are back ({secs} s).", back.len(), if back.len() == 1 { "" } else { "s" })); }-        else { toast(format!("pup updated to {to}"), format!("{} of {} windows are back. Not back yet: {}. pup keeps trying in the background.", back.len(), ids.len(), missing.join(", "))); }+        let you_closed = if closed.is_empty() { String::new() } else { format!(" You closed {}.", closed.join(", ")) };+        if missing.is_empty() { toast(format!("pup updated to {to}"), format!("{} window{} back ({secs} s).{you_closed}", back.len(), if back.len() == 1 { " is" } else { "s are" })); }+        else { toast(format!("pup updated to {to}"), format!("{} of {} windows are back.{you_closed} Not back yet: {}; pup keeps trying in the background.", back.len(), ids.len(), missing.join(", "))); }         pup_core::log(format!("[extdrive] update cycle finished in {secs} s: {} of {} window(s) back{}", back.len(), ids.len(), if missing.is_empty() { String::new() } else { format!("; not yet: {}", missing.join(", ")) }));     } }@@ -391,7 +394,7 @@ fn aim(c: &mut Value) {  /// pup_open_window {url} — launch (or reuse) the extension-driven browser and open `url` in it. pub fn open(ctx: &Ctx, _caller: &Caller, args: &Value) -> Value {-    let r = with_lane_progress(ctx, || open_inner(ctx, _caller, args));+    let r = with_lane_progress(ctx, _caller, args, || open_inner(ctx, _caller, args));     if r["ok"] != json!(false) { dress_after(ctx, _caller); }     r }@@ -437,7 +440,7 @@ fn open_inner(ctx: &Ctx, _caller: &Caller, args: &Value) -> Value {  /// pup_navigate {url, tabId?} pub fn navigate(ctx: &Ctx, _caller: &Caller, args: &Value) -> Value {-    let r = with_lane_progress(ctx, || navigate_inner(ctx, _caller, args));+    let r = with_lane_progress(ctx, _caller, args, || navigate_inner(ctx, _caller, args));     if r["ok"] != json!(false) { dress_after(ctx, _caller); }     r }@@ -530,7 +533,7 @@ pub fn diag(ctx: &Ctx, _caller: &Caller, args: &Value) -> Value { } /// pup_open_tab {url} — open a TAB in the extension-driven window (the lane could only make new windows). pub fn tab(ctx: &Ctx, _caller: &Caller, args: &Value) -> Value {-    let r = with_lane_progress(ctx, || tab_inner(ctx, _caller, args));+    let r = with_lane_progress(ctx, _caller, args, || tab_inner(ctx, _caller, args));     if r["ok"] != json!(false) { dress_after(ctx, _caller); }     r }@@ -805,11 +808,12 @@ pub(crate) fn lane_hwnd(ctx: &Ctx) -> Option<i64> { /// bars in the icon. and why do i not see the overlay icon"). Same ab calls activity.rs makes: /// an indeterminate bar while a command runs, cleared when it ends; taskbarActivity off/flash and /// minimalTouch are honoured exactly as there.-fn with_lane_progress<T>(ctx: &Ctx, work: impl FnOnce() -> T) -> T {-    // ONE progress implementation, in activity.rs. This is just the lane's entry point: find the lane-    // window and hand it the same filling estimated bar the CDP lane gets. My first cut hand-rolled a-    // flat 35% bar and threw ab's reply away; deleted.-    match lane_hwnd(ctx) {+fn with_lane_progress<T>(ctx: &Ctx, caller: &Caller, args: &Value, work: impl FnOnce() -> T) -> T {+    // ONE progress implementation, in activity.rs. This is just the lane's entry point. 2.0.699: the bar goes+    // on the window the command is FOR. It used to go on lane_hwnd, the first window Windows listed, so with+    // several threads' windows open it painted another thread's button. A window being born has no handle+    // yet: no bar here (start_icon_apply covers a new window's first seconds).+    match crate::resolve(ctx, caller, args).ok().and_then(|s| s.hwnd) {         Some(hwnd) => crate::activity::progress_on_hwnd(ctx, hwnd, "ext-drive", work),         None => work(),     }
rust/verbs/src/lib.rs+14−6
@@ -597,8 +597,8 @@ pub fn dispatch(ctx: &Ctx, caller: &Caller, verb: &str, args: &Value) -> Value {         "pup_status" => pup_core::merge(status(ctx), json!({ "extensionLane": extdrive::lane_health(ctx) })),         "pup_list_windows" => list_windows(ctx),         "pup_my_windows" | "pup_list_sessions" => my_windows(ctx, caller),-        "pup_open_window" => open_window(ctx, caller, args),-        "pup_navigate" => navigate(ctx, caller, args),+        "pup_open_window" => with_bar(ctx, caller, args, "open", || open_window(ctx, caller, args)),+        "pup_navigate" => with_bar(ctx, caller, args, "navigate", || navigate(ctx, caller, args)),         "pup_eval" | "pup_evaluate" => eval(ctx, caller, args),  // frame: handled inside eval         "pup_screenshot" => { match resolve(ctx, caller, args) { Ok(s) => { let sid = s.id.clone(); activity::with_progress(ctx, &sid, "screenshot", || screenshot(ctx, caller, args)) } Err(_) => screenshot(ctx, caller, args) } },         "pup_close_window" | "pup_close" => close_window(ctx, caller, args),@@ -606,7 +606,7 @@ pub fn dispatch(ctx: &Ctx, caller: &Caller, verb: &str, args: &Value) -> Value {         // supported equivalent. close_window already implements {all:true} = the caller's own windows.         "pup_close_all_windows" => { let mut a = args.clone(); if let Some(m) = a.as_object_mut() { m.insert("all".into(), json!(true)); } close_window(ctx, caller, &a) }         "pup_window_info" => window_info(ctx, caller, args),-        "pup_open_tab" => tabs::open_tab(ctx, caller, args),+        "pup_open_tab" => with_bar(ctx, caller, args, "open-tab", || tabs::open_tab(ctx, caller, args)),         "pup_list_tabs" => tabs::list_tabs(ctx, caller, args),         "pup_switch_tab" => tabs::switch_tab(ctx, caller, args),         "pup_close_tab" => tabs::close_tab(ctx, caller, args),@@ -619,9 +619,9 @@ pub fn dispatch(ctx: &Ctx, caller: &Caller, verb: &str, args: &Value) -> Value {         "pup_fill" => input::fill(ctx, caller, args),         "pup_select" => input::select(ctx, caller, args),         "pup_wait" => input::wait(ctx, caller, args),-        "pup_reload" => input::reload(ctx, caller, args),-        "pup_back" => input::history(ctx, caller, args, true),-        "pup_forward" => input::history(ctx, caller, args, false),+        "pup_reload" => with_bar(ctx, caller, args, "navigate", || input::reload(ctx, caller, args)),+        "pup_back" => with_bar(ctx, caller, args, "navigate", || input::history(ctx, caller, args, true)),+        "pup_forward" => with_bar(ctx, caller, args, "navigate", || input::history(ctx, caller, args, false)),         "pup_set_viewport" => input::set_viewport(ctx, caller, args),         "pup_errors" => input::errors(ctx, caller, args),         "pup_dialog" => input::dialog(ctx, caller, args),@@ -832,6 +832,14 @@ pub fn session_for_tab_url(ctx: &Ctx, url: &str) -> Option<(Session, Tab)> {     best.map(|(_, s, t)| (s, t)) } +/// 2.0.699: the taskbar progress bar on the window a page-loading verb works in. Only pup_screenshot was ever+/// wrapped here; the extension-lane wrapper sat on a route these verbs no longer take, so open, navigate, new+/// tab, reload, back and forward painted nothing (John, 2026-10-05). A window with no handle yet (being born)+/// gets no bar; start_icon_apply covers its first seconds.+fn with_bar(ctx: &Ctx, caller: &Caller, args: &Value, op: &str, work: impl FnOnce() -> Value) -> Value {+    match resolve(ctx, caller, args) { Ok(s) => activity::with_progress(ctx, &s.id, op, work), Err(_) => work() }+}+ pub fn resolve(ctx: &Ctx, caller: &Caller, args: &Value) -> Result<Session, Value> {     let explicit = window_arg(args);     let id = match explicit.clone() {
rust/verbs/src/recovery.rs+12
@@ -243,6 +243,18 @@ pub fn relaunch_from_file(ctx: &Ctx, f: &Value) {             }         }     }+    // 2.0.698: a restored window reloads every page at once, and every video in them autoplayed together ("i get like+    // 10 videos playing all at the same time", John 2026-10-04). Pause media that starts on its own in the restored+    // 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("?"))),+            }+        }+    }     // 2.0.471: a relaunch that WORKED resets the counter. It counted every relaunch in the bridge's life, so a window     // brought back twice (a wake, a crash) was refused the third time as "failed to come back twice" (winvm drill,     // 2026-09-11: "drill-b" back with 0 of 3 after pup_restart_browser). Only consecutive failures count now.
src/extension/annotate-overlay.js+25−4
@@ -10,7 +10,8 @@ window.__pupStartAnnotate = function (initTool, initMode) {   // v2.0.370 (John): two explicit capture modes. quick=true (Auto, the default) captures the   // moment the mouse lifts — the one-red-circle case. quick=false (Multi) accumulates shapes —   // a red circle here, a blue circle there — and captures ONCE on Done.-  var S = { tool: 'pen', color: '#f85149', strokes: [], cur: null, quick: initMode !== 'multi', seq: 0 };+  // 2.0.701 (John, 2026-10-06): MULTI is the default; Single (the old Auto) is one keypress away.+  var S = { tool: 'pen', color: '#f85149', strokes: [], cur: null, quick: initMode === 'auto', seq: 0 };   var wrap = document.createElement('div');   wrap.id = '__pup_annotate';   wrap.style.cssText = 'position:fixed;inset:0;z-index:2147483646;pointer-events:none;';@@ -263,11 +264,19 @@ window.__pupStartAnnotate = function (initTool, initMode) {     modeWrap.appendChild(b); tipFor(b, tip);     return b;   }-  var mAuto = modeBtn('Auto', 'auto', true, 'One-shot (the default): draw one thing, lift the mouse, and it is captured and copied to your clipboard instantly.');-  var mMulti = modeBtn('Multi', 'multi', false, 'Compose: draw several shapes and NOTHING is captured until you click Done. The color auto-advances after each shape (red, blue, yellow, green, purple) so every shape is distinct — click a swatch any time to override.');+  var mAuto = modeBtn('Single', 'auto', true, 'Single (S): draw one thing, lift the mouse, and it is captured and copied to your clipboard instantly. Space switches back to Multi.');+  var mMulti = modeBtn('Multi', 'multi', false, 'Multi (the default, M): draw several shapes and NOTHING is captured until you press Enter or click Done. The color auto-advances after each shape (red, blue, yellow, green, purple) so every shape is distinct — click a swatch any time to override.');+  // 2.0.701: a one-line hint under the toolbar that says how to finish in the current mode and how to switch.+  var hint = document.createElement('div');+  hint.style.cssText = 'position:absolute;top:100%;left:50%;transform:translateX(-50%);margin-top:6px;white-space:nowrap;background:#161b22e6;border:1px solid #30363d;border-radius:999px;padding:3px 11px;font:12px Satoshi,system-ui,sans-serif;color:#8b949e;pointer-events:none;';+  bar.style.position = bar.style.position || 'fixed'; bar.appendChild(hint);+  function kbd(k) { return '<b style="color:#c9d1d9;font-weight:600">' + k + '</b>'; }   function paintMode() {     mAuto.style.background = S.quick ? 'rgba(0,184,177,.13)' : 'none'; mAuto.style.color = S.quick ? '#00d4cb' : '#c9d1d9';     mMulti.style.background = !S.quick ? 'rgba(0,184,177,.13)' : 'none'; mMulti.style.color = !S.quick ? '#00d4cb' : '#c9d1d9';+    hint.innerHTML = S.quick+      ? 'Single: lift the mouse to capture \u00b7 ' + kbd('Space') + ' for Multi \u00b7 ' + kbd('Esc') + ' to cancel'+      : 'Multi: ' + kbd('Enter') + ' to finish \u00b7 ' + kbd('Space') + ' for Single \u00b7 ' + kbd('Esc') + ' to cancel';   }   paintMode(); bar.appendChild(modeWrap);   btn('Undo', 'Remove the last thing you drew. Click again to keep unwinding.', function () { S.strokes.pop(); redraw(); }, null, 'undo');@@ -276,7 +285,19 @@ window.__pupStartAnnotate = function (initTool, initMode) {   done.style.cssText += 'background:rgba(0,184,177,.13);border:1px solid rgba(0,184,177,.55);color:#00d4cb;font-weight:600;';   btn('', 'Close the annotation toolbar and remove all drawings from the page (Esc does this too).', function () { exitAll(); }, null, 'cancel');   function exitAll() { disarmKeys(); wrap.remove(); window.__pupAnnotate = null; window.__pupAnnotateResult = { done: false, ts: Date.now() }; }-  function onKey(e) { if (e.key === 'Escape' || e.keyCode === 27) { e.preventDefault(); e.stopPropagation(); exitAll(); } }+  // 2.0.701 (John): Enter finishes, Space toggles Multi/Single, M and S pick one. Esc still cancels. Keys with a+  // modifier held are left to the page and the browser.+  function onKey(e) {+    if (e.key === 'Escape' || e.keyCode === 27) { e.preventDefault(); e.stopPropagation(); exitAll(); return; }+    if (e.ctrlKey || e.metaKey || e.altKey) return;+    var k = (e.key || '').toLowerCase(), hit = true;+    if (k === 'enter') { if (S.strokes.length) emit(null, true); }+    else if (k === ' ' || k === 'spacebar') { S.quick = !S.quick; paintMode(); }+    else if (k === 'm') { S.quick = false; paintMode(); }+    else if (k === 's') { S.quick = true; paintMode(); }+    else hit = false;+    if (hit) { e.preventDefault(); e.stopPropagation(); }+  }   // Esc reaches the overlay wherever focus sits: window and document in the capture phase, the canvas itself,   // and the canvas takes focus when armed (a focused iframe would otherwise keep the key to itself).   function armKeys() { addEventListener('keydown', onKey, true); document.addEventListener('keydown', onKey, true); cv.addEventListener('keydown', onKey, true); try { cv.tabIndex = -1; cv.style.outline = 'none'; cv.focus({ preventScroll: true }); } catch (e) {} }
src/extension/worker.js+24
@@ -429,6 +429,30 @@ async function pupExtPollOnce() {       await new Promise((r) => setTimeout(r, 350));       const cap = await pupCaptureTab(t);       out = { id: cmd.id, ok: true, tabId: t.id, dataUrl: cap.dataUrl, via: cap.via };+    } else if (cmd.action === 'quietMedia') {+      // 2.0.698: a restored window reloads every page at once and every video autoplayed together. For `ms` after+      // each tab loads, pause media that starts on its own; once the user has clicked or typed in the page, leave it.+      const ms = Number(cmd.ms) || 30000;+      const tabs = await chrome.tabs.query({ windowId: cmd.windowId });+      const guard = (span) => {+        if (window.__pupQuiet) return 0;+        window.__pupQuiet = true;+        const stopAt = Date.now() + span;+        const user = () => !!(navigator.userActivation && navigator.userActivation.hasBeenActive);+        const pauseAll = () => { let n = 0; document.querySelectorAll('video,audio').forEach((m) => { if (!m.paused) { try { m.pause(); n++; } catch (e) {} } }); return n; };+        const onPlay = (e) => { if (Date.now() > stopAt || user()) { document.removeEventListener('play', onPlay, true); return; } try { e.target.pause(); } catch (x) {} };+        document.addEventListener('play', onPlay, true);+        const iv = setInterval(() => { if (Date.now() > stopAt || user()) { clearInterval(iv); return; } pauseAll(); }, 500);+        return pauseAll();+      };+      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] });+          return (r || []).reduce((a, x) => a + (Number(x && x.result) || 0), 0);+        } catch (e) { return -1; }+      }));+      out = { id: cmd.id, ok: true, guarded: results.filter((x) => x >= 0).length, paused: results.filter((x) => x > 0).reduce((a, x) => a + x, 0) };     } else if (cmd.action === 'tabs') {       const tabs = await chrome.tabs.query({});       out = { id: cmd.id, ok: true, tabs: tabs.map((t) => ({ tabId: t.id, url: t.url, title: t.title, active: !!t.active, windowId: t.windowId, favIconUrl: t.favIconUrl || null, openerTabId: t.openerTabId ?? null })) };