← Commit history
CHANGELOG.md+18
@@ -1,5 +1,23 @@ # Changelog — Puppeteer Bridge +## 2.0.425 — 2026-09-06 — the taskbar activity contract, as Node had it (no orange by default)++John: "go look at how we had setup the windows taskbar flashes in the node version and what our+defaults are, cuz you seemed to have strayed from that." Node 2.0.289 to 2.0.304: ONE setting,+`taskbarActivity` = progress (default) | flash | off; openFlash, autoFlash, agentActivity and fgWatch+were removed in 2.0.292. The Rust port still carried those dead keys, painted no activity signal at all,+left every background-born button wearing the Windows 11 attention tint (the pink buttons), and made+pup_alert_window an orange flash regardless of mode. Restored:++- progress mode never paints orange. After birth pup clears the attention tint through ab's+  `desktop_clear_window_attention` (off-screen activate-and-restore, net-zero focus). The icon-apply bar+  runs from open until the icon lands; pup_screenshot carries a bar sized to its running average+  (persisted in `~/.adom/pup-verb-timing.json`, EMA 0.75/0.25, capped at 92% until the work is done);+  pup_alert_window is a 2.5 s full-bar pulse. Gated by minimalTouch as in Node.+- flash mode: the finite four-count orange flash on open and on alert, never a flash-forever.+- stop: FLASHW_STOP plus the attention clear, and the response says whether the tint truly cleared.+- pup_configure refuses openFlash / autoFlash / agentActivity / fgWatch with the Node message.+ ## 2.0.424 — 2026-09-06 — jump-list tasks actually work again (human callers bypass ownership); thin LTO  John: "have you ralph tested your jump list items? i just tried annotate and it doesn't work." Measured:
docs/PLAN-rust-port.md+4−1
@@ -114,7 +114,10 @@ John's words. Written 2026-09-05 against pup 2.0.396; John approved the plan the   so every window-targeting task had been refused since 2.0.407. Fixed (USER_CALLERS bypass ownership) and   gated: `tools/jumplist-ralph.py --target <box>` fires every task through ab's taskrun shim with the link's   exact target and arguments and asserts the effect (6/6 on arav-rog). Lesson recorded: testing the exe by-  hand is not testing the jump list. Release profile: thin LTO (28 s vs 36.5 s per leaf edit, exe +0.8 MB). Jump lists: the two-right-clicks flash is ab#155+  hand is not testing the jump list. Release profile: thin LTO (28 s vs 36.5 s per leaf edit, exe +0.8 MB).+  2.0.425: the taskbar activity contract restored from Node (taskbarActivity progress|flash|off, dead keys+  refused, attention tint cleared after birth, icon-apply and screenshot progress bars, alert = pulse in+  progress mode, four-count flash in flash mode). John: "you seemed to have strayed from that"; he had. Jump lists: the two-right-clicks flash is ab#155   (registry-only AUMIDs, no anchoring shortcut); `aumidShortcut` (opt-in) asks ab for the anchor;   human verification pending. Filed: ab#152 (kind:exe seed npm), ab#155 (jump-list anchor), ab#157   (forward bridge consent prompts into ah for boxes that block toasts).
rust/session/src/lib.rs+2−1
@@ -50,6 +50,7 @@ pub struct Session {     pub flash_count: u32,     pub progress_count: u32,     pub progress_ms: u64,+    pub icon_applied: bool,     pub owner_from_caller: bool,     pub disruption_notice: Option<String>,     pub fg_watch_until_ms: u64,@@ -62,7 +63,7 @@ impl Session {     /// Node: session._lastAgentUpdate = {tabId, verb, note, ts} | null.     pub fn last_agent_update(&self) -> Value { match &self.last_verb { Some(v) => json!({ "tabId": self.last_tab, "verb": v, "note": self.last_note, "ts": self.last_agent_update_ms }), None => Value::Null } }     pub fn new(id: &str, profile: &str, owner: Option<&str>, owner_container: Option<&str>, cdp_port: u16, pid: Option<u32>) -> Session {-        Session { id: id.into(), profile: profile.into(), owner: owner.map(|s| s.into()), owner_container: owner_container.filter(|s| !s.is_empty()).map(|s| s.into()), created_ms: pup_core::now_ms(), created_iso: pup_core::iso_now(), hwnd: None, pid, cdp_port, tabs: Vec::new(), active_tab: 0, foreground: false, app_id: None, last_agent_update_ms: pup_core::now_ms(), next_tab_seq: 1, emulated: None, last_note: None, pending_cred_hint: None, wiki_authed: None, wiki_view: None, last_annotation: None, last_verb: None, last_tab: None, last_foreground: None, overlay_desc: None, flash_count: 0, progress_count: 0, progress_ms: 0, owner_from_caller: false, disruption_notice: None, fg_watch_until_ms: 0, last_outer_width: None, last_url: None, last_title: None }+        Session { id: id.into(), profile: profile.into(), owner: owner.map(|s| s.into()), owner_container: owner_container.filter(|s| !s.is_empty()).map(|s| s.into()), created_ms: pup_core::now_ms(), created_iso: pup_core::iso_now(), hwnd: None, pid, cdp_port, tabs: Vec::new(), active_tab: 0, foreground: false, app_id: None, last_agent_update_ms: pup_core::now_ms(), next_tab_seq: 1, emulated: None, last_note: None, pending_cred_hint: None, wiki_authed: None, wiki_view: None, last_annotation: None, last_verb: None, last_tab: None, last_foreground: None, overlay_desc: None, flash_count: 0, progress_count: 0, progress_ms: 0, icon_applied: false, owner_from_caller: false, disruption_notice: None, fg_watch_until_ms: 0, last_outer_width: None, last_url: None, last_title: None }     }     pub fn next_tab_id(&mut self) -> String { let id = format!("tab-{}", self.next_tab_seq); self.next_tab_seq += 1; id }     pub fn active(&self) -> Option<&Tab> { self.tabs.get(self.active_tab) }
rust/verbs/src/activity.rsadded+118
@@ -0,0 +1,118 @@+//! The taskbar ACTIVITY signal (Node v2.0.289 to v2.0.304, `taskbarActivity` = progress | flash | off,+//! default progress). In `progress` mode pup NEVER paints orange: a determinate bar sized to the+//! running average of each operation (icon apply from open until the icon lands, screenshots), an+//! alert is a short full-bar pulse, and the Windows 11 attention tint a background-born window gets+//! is cleared through ab's `desktop_clear_window_attention` (FLASHW_STOP alone never clears it).+//! `flash` = the old finite four-count orange flash on open and on alert. `off` = nothing.+//! openFlash / autoFlash / agentActivity were removed in Node 2.0.292; the Rust port carried them+//! as dead keys until 2.0.425 (John, 2026-09-06: "you seemed to have strayed from that").++use crate::Ctx;+use pup_core::Caller;+use serde_json::{json, Value};+use std::collections::HashMap;+use std::sync::{Arc, Mutex};+use std::time::Duration;++const DEFAULT_EST_MS: [(&str, u64); 4] = [("icon-apply", 2200), ("screenshot", 900), ("navigate", 2500), ("open", 3200)];+static TIMING: Mutex<Option<HashMap<String, (u64, u64)>>> = Mutex::new(None);   // op -> (n, avgMs), persisted++pub fn mode(ctx: &Ctx) -> String { let m = ctx.settings.get("taskbarActivity", "progress"); if m == "flash" || m == "off" { m } else { "progress".into() } }+fn minimal_touch(ctx: &Ctx) -> bool { ctx.settings.get("minimalTouch", "on") == "on" }+fn timing_file(ctx: &Ctx) -> std::path::PathBuf { ctx.host.state_dir().join("pup-verb-timing.json") }++fn load(ctx: &Ctx) {+    let mut g = TIMING.lock().unwrap();+    if g.is_some() { return; }+    let mut m = HashMap::new();+    if let Ok(s) = std::fs::read_to_string(timing_file(ctx)) { if let Ok(v) = serde_json::from_str::<Value>(&s) { if let Some(o) = v.as_object() { for (k, e) in o { m.insert(k.clone(), (e["n"].as_u64().unwrap_or(0), e["avgMs"].as_u64().unwrap_or(0))); } } } }+    *g = Some(m);+}+pub fn estimate_ms(ctx: &Ctx, op: &str) -> u64 {+    load(ctx);+    let g = TIMING.lock().unwrap();+    g.as_ref().and_then(|m| m.get(op)).filter(|(n, a)| *n > 0 && *a > 0).map(|(_, a)| *a)+        .unwrap_or_else(|| DEFAULT_EST_MS.iter().find(|(k, _)| *k == op).map(|(_, v)| *v).unwrap_or(1200))+}+fn record(ctx: &Ctx, op: &str, ms: u64) {+    if ms > 120_000 { return; }+    load(ctx);+    let snapshot = {+        let mut g = TIMING.lock().unwrap(); let m = g.as_mut().unwrap();+        let e = m.entry(op.to_string()).or_insert((0, ms));+        e.1 = if e.0 == 0 { ms } else { (e.1 * 3 + ms) / 4 };   // Node: avg*0.75 + ms*0.25, recent runs weigh more+        e.0 = (e.0 + 1).min(100_000);+        json!(m.iter().map(|(k, (n, a))| (k.clone(), json!({ "n": n, "avgMs": a }))).collect::<serde_json::Map<_, _>>())+    };+    let _ = std::fs::write(timing_file(ctx), snapshot.to_string());+}++fn push(ctx: &Ctx, hwnd: i64, value: Option<u64>) {+    let p = match value { Some(v) => json!({ "state": "normal", "value": (v.min(100) as f64) / 100.0 }), None => json!({ "state": "none" }) };+    let _ = ctx.ab.call("desktop_taskbar", json!({ "hwnd": hwnd, "progress": p }), &crate::identity::internal_caller(), Duration::from_secs(3));+}++/// Node withTaskbarProgress: a bar that fills over the running-average estimate for `op`, capped at+/// 92% until the work really finishes, then 100% and cleared. Records the true duration.+pub fn with_progress<T>(ctx: &Ctx, sid: &str, op: &str, work: impl FnOnce() -> T) -> T {+    if !cfg!(windows) || mode(ctx) != "progress" || minimal_touch(ctx) { return work(); }+    let Some(hwnd) = ctx.sessions.get(sid).and_then(|s| s.hwnd) else { 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();+    push(ctx, hwnd, Some(2));+    std::thread::Builder::new().name("taskbar-progress".into()).spawn(move || {+        while !d2.load(std::sync::atomic::Ordering::Relaxed) {+            std::thread::sleep(Duration::from_millis(180));+            if d2.load(std::sync::atomic::Ordering::Relaxed) { break; }+            let f = (started.elapsed().as_millis() as f64 / est as f64).min(0.92);+            push(&arc, hwnd, Some(((f * 100.0) as u64).max(2)));+        }+    }).ok();+    let r = work();+    done.store(true, std::sync::atomic::Ordering::Relaxed);+    record(ctx, op, started.elapsed().as_millis() as u64);+    push(ctx, hwnd, Some(100));+    if let Some(arc) = ctx.arc() { std::thread::spawn(move || { std::thread::sleep(Duration::from_millis(220)); push(&arc, hwnd, None); }); }+    ctx.sessions.with_mut(sid, |x| { x.progress_count += 1; x.progress_ms += started.elapsed().as_millis() as u64; });+    r+}++/// 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; }+    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;+        for _ in 0..40 {+            let Some(s) = ctx.sessions.get(&sid) else { break };+            if s.icon_applied { landed = true; break; }+            if hwnd.is_none() { hwnd = s.hwnd; }+            if let Some(h) = hwnd { let f = (started.elapsed().as_millis() as f64 / est as f64).min(0.92); push(&ctx, h, Some(((f * 100.0) as u64).max(2))); }+            std::thread::sleep(Duration::from_millis(200));+        }+        if let Some(h) = hwnd { if landed { record(&ctx, "icon-apply", started.elapsed().as_millis() as u64); push(&ctx, h, Some(100)); std::thread::sleep(Duration::from_millis(220)); } push(&ctx, h, None); }+    }).ok();+}+pub fn mark_icon_applied(ctx: &Ctx, sid: &str) { ctx.sessions.with_mut(sid, |x| x.icon_applied = true); }++/// Node osClearFlashByActivate (v1.8.45 / v2.0.27): the Windows 11 attention tint a background-born+/// window wears (the OS flashes a window it denied the foreground) clears only on activation; ab's+/// `desktop_clear_window_attention` does the off-screen activate-and-restore with net-zero focus.+/// Skipped when the window already IS the foreground (nothing to clear, and it would be a no-op).+pub fn clear_attention(ctx: &Ctx, hwnd: i64, caller: &Caller) -> String {+    if !cfg!(windows) { return "not-windows".into(); }+    if let Ok(fg) = ctx.ab.call("desktop_get_foreground", json!({}), caller, Duration::from_secs(4)) { if fg["hwnd"].as_i64() == Some(hwnd) { return "already-foreground".into(); } }+    match ctx.ab.call("desktop_clear_window_attention", json!({ "hwnd": hwnd }), caller, Duration::from_secs(8)) {+        Ok(r) => if r["cleared"].as_bool() == Some(true) || r["ok"].as_bool() == Some(true) { "cleared".into() } else { "error".into() },+        Err(e) => format!("error: {e}"),+    }+}++/// The birth signal: `flash` = finite four-count orange flash (never forever); `progress` = the+/// icon-apply bar (started by the caller) plus the attention-tint clear; `off` = only the clear.+pub fn on_open(ctx: &Ctx, sid: &str, hwnd: i64, caller: &Caller) {+    if !cfg!(windows) { return; }+    if mode(ctx) == "flash" && !minimal_touch(ctx) { let _ = ctx.ab.call("desktop_flash_window", json!({ "hwnd": hwnd, "mode": "count", "count": 4 }), caller, Duration::from_secs(6)); ctx.sessions.with_mut(sid, |x| x.flash_count += 1); }+}
rust/verbs/src/identity.rs+1−1
@@ -118,7 +118,7 @@ pub fn stamp(ctx: &Ctx, sid: &str, caller: &Caller, full: bool) {         if let Err(e) = ctx.ab.call("desktop_register_app_identity", register_args(ctx, &app_id, &display, &icon.to_string_lossy()), caller, AB_T) { pup_core::log(format!("[identity] \"{sid}\" register {app_id} failed: {e}")); return; }     let launch = exe_launch();         match ctx.ab.call("desktop_set_window_identity", json!({ "hwnd": hwnd, "appId": app_id, "displayName": display, "iconPath": icon.to_string_lossy(), "relaunchCommand": id::relaunch_command(&launch, sid), "relaunchDisplayName": display, "relaunchIconResource": icon.to_string_lossy() }), caller, AB_T) {-            Ok(_) => { pup_core::log(format!("[identity] \"{sid}\" stamped {app_id} ({m})")); ctx.sessions.with_mut(sid, |x| x.app_id = Some(app_id.clone())); if let Some(x) = ctx.sessions.get(sid) { ctx.sessions.save_file(&x); } }+            Ok(_) => { pup_core::log(format!("[identity] \"{sid}\" stamped {app_id} ({m})")); ctx.sessions.with_mut(sid, |x| x.app_id = Some(app_id.clone())); crate::activity::mark_icon_applied(ctx, sid); if let Some(x) = ctx.sessions.get(sid) { ctx.sessions.save_file(&x); } }             Err(e) => { pup_core::log(format!("[identity] \"{sid}\" stamp failed: {e}")); return; }         }         if ctx.settings.get("jumpLists", "on") != "off" {
rust/verbs/src/lib.rs+8−1
@@ -19,6 +19,7 @@ use std::time::{Duration, Instant};  pub mod image; pub mod events;+pub mod activity; pub mod tabs; pub mod input; pub mod keys;@@ -213,7 +214,7 @@ pub fn dispatch(ctx: &Ctx, caller: &Caller, verb: &str, args: &Value) -> Value {         "pup_open_window" => open_window(ctx, caller, args),         "pup_navigate" => navigate(ctx, caller, args),         "pup_eval" | "pup_evaluate" => eval(ctx, caller, args),  // frame: handled inside eval-        "pup_screenshot" => screenshot(ctx, caller, args),+        "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),         "pup_window_info" => window_info(ctx, caller, args),         "pup_open_tab" => tabs::open_tab(ctx, caller, args),@@ -682,6 +683,12 @@ fn open_window(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {         ctx.host.set_bounds_no_activate(h, bl, bt, bw, bh);         parked = ctx.host.send_to_bottom_no_activate(h);         let _ = ctx.ab.call("desktop_set_window_state", json!({ "hwnd": h, "state": "bottom", "force": true }), caller, Duration::from_secs(5));+        // Node: a background birth leaves the Windows 11 attention tint on the button; clear it now+        // (ab's off-screen activate-and-restore, net-zero focus), then the activity signal for this mode.+        let cleared = activity::clear_attention(ctx, h, caller);+        pup_core::log(format!("[window] \"{sid}\" attention tint: {cleared}"));+        activity::on_open(ctx, &sid, h, caller);+        if let Some(a) = ctx.arc() { activity::start_icon_apply(a, sid.clone()); }     }     if let Some(cs) = s.tabs.last().map(|t| t.cdp_session.clone()) { inject_page_scripts(ctx, &s, &b.cdp, &cs); }     let loaded = if let Some(t) = s.tabs.last_mut() { let r = b.cdp.navigate(&t.cdp_session, &url, Duration::from_secs(20)).map(|(_, l)| l).unwrap_or(false); refresh_tab(&b.cdp, t); r } else { false };
rust/verbs/src/settings.rs+3−1
@@ -13,7 +13,7 @@ pub const KEYS: &[(&str, &str, &str)] = &[     ("jumpLists", "on", "on|off"), ("jlViewTasks", "on", "on|off"), ("jlCloseAll", "on", "on|off"), ("jlDashboard", "on", "on|off"), ("jlAnnotate", "on", "on|off"),     ("toolbarExtension", "on", "on|off"), ("tbSnipHotkey", "on", "on|off"), ("tbFloatyPencil", "off", "on|off"), ("floatyHitPx", "24", "int"), ("tbOwnerBadge", "on", "on|off"),     ("annCaptionBar", "on", "on|off"),-    ("openFlash", "on", "on|off"), ("autoFlash", "on", "on|off"), ("agentActivity", "on", "on|off"), ("taskbarActivity", "progress", "progress|flash|off"),+    ("taskbarActivity", "progress", "progress|flash|off"),     ("fgWatch", "on", "on|off"), ("aumidShortcut", "off", "on|off"), ("fgCaption", "on", "on|off"), ("fgCaptionMs", "3500", "int"), ("notMineCaption", "off", "on|off"),     ("captureLogins", "on", "on|off"), ("autoLogin", "on", "on|off"),     ("ownerChip", "on", "on|off"), ("titleTag", "off", "on|off"), ("titlePrefix", "off", "on|off"),@@ -33,6 +33,8 @@ pub fn configure(ctx: &Ctx, args: &Value) -> Value {     let mut changed = Vec::new(); let mut rejected = Vec::new();     if let Some(o) = args.as_object() {         for (k, v) in o {+            if k == "caller" || k.starts_with('_') { continue; }   // CLI/relay bookkeeping, not settings+            if ["openFlash", "autoFlash", "agentActivity", "fgWatch"].contains(&k.as_str()) { rejected.push(json!({ "key": k, "why": format!("{k} was removed in 2.0.292; use taskbarActivity: \"progress\"|\"flash\"|\"off\" (and notMineCaption for the foreground caption)") })); continue }             let Some((_, _, allowed)) = KEYS.iter().find(|(kk, _, _)| kk == k) else { rejected.push(json!({ "key": k, "why": "unknown setting" })); continue };             let sv = match v { Value::String(s) => s.clone(), Value::Bool(b) => if *b { "on".into() } else { "off".into() }, Value::Number(n) => n.to_string(), _ => { rejected.push(json!({ "key": k, "why": "value must be a string, bool or number" })); continue } };             let okv = match *allowed { "int" => sv.parse::<i64>().is_ok(), "str" => true, list => list.split('|').any(|a| a == sv) };
rust/verbs/src/window.rs+19−4
@@ -55,10 +55,25 @@ pub fn alert(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {     let s = match resolve(ctx, caller, args) { Ok(s) => s, Err(e) => return e };     let Some(h) = hwnd_of(ctx, &s) else { return err("no_window_handle", "pup has no OS window handle for this window yet", None) };     let stop = args.get("stop").and_then(|b| b.as_bool()).unwrap_or(false);-    let r = if stop { ctx.ab.call("desktop_flash_window", json!({ "hwnd": h, "stop": true }), caller, AB_T) } else { ctx.ab.call("desktop_flash_window", json!({ "hwnd": h }), caller, AB_T) };-    let applied = r.is_ok();-    let ab_error = r.as_ref().err().map(|e| e.to_string());-    ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "alerted": !stop && applied, "cleared": stop && applied, "applied": applied, "abError": ab_error, "clearMethod": if stop { "desktop_flash_window stop" } else { Value::Null.as_str().unwrap_or("") }, "_hint": if stop { "Taskbar highlight cleared." } else { "Taskbar button flashing (no focus stolen). The user notices it on their own time." } }))+    let mode = crate::activity::mode(ctx);+    if stop {+        // Node v1.8.45: FLASHW_STOP only stops the pulse; the Win11 tint needs the activate-and-restore.+        let r = ctx.ab.call("desktop_flash_window", json!({ "hwnd": h, "stop": true }), caller, AB_T);+        let cleared = crate::activity::clear_attention(ctx, h, caller);+        let truly = cleared == "cleared" || cleared == "already-foreground";+        return ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "alerted": false, "cleared": truly, "applied": r.is_ok(), "clearMethod": cleared, "_hint": if truly { "Flash truly cleared: activated the window off-screen to drop the Win11 attention tint, then handed focus back. No pop, no focus steal." } else { "FLASHW_STOP ran but the persistent tint needs activation and that step failed." } }));+    }+    if mode == "progress" {+        // Node v2.0.304 (John: "i thought our new setting was for you to not do the orange flash anymore"):+        // in progress mode the nudge is a short full-bar pulse, never orange.+        let _ = ctx.ab.call("desktop_taskbar", json!({ "hwnd": h, "progress": { "state": "normal", "value": 1.0 } }), caller, AB_T);+        if let Some(a) = ctx.arc() { std::thread::spawn(move || { std::thread::sleep(Duration::from_millis(2500)); let _ = a.ab.call("desktop_taskbar", json!({ "hwnd": h, "progress": { "state": "none" } }), &crate::identity::internal_caller(), AB_T); }); }+        return ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "alerted": true, "cleared": false, "applied": true, "method": "progress-pulse", "_hint": "taskbarActivity is \"progress\", so the nudge was a short full progress pulse on the button (no orange). pup_configure {taskbarActivity:\"flash\"} brings the orange flash back." }));+    }+    if mode == "off" { return ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "alerted": false, "applied": false, "_hint": "taskbarActivity is \"off\": no taskbar signal is painted." })); }+    let r = ctx.ab.call("desktop_flash_window", json!({ "hwnd": h, "mode": "count", "count": 4 }), caller, AB_T);+    let applied = r.is_ok(); ctx.sessions.with_mut(&s.id, |x| x.flash_count += 1);+    ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "alerted": applied, "cleared": false, "applied": applied, "abError": r.as_ref().err().map(|e| e.to_string()), "_hint": "Flashed the taskbar (finite 4-count) via ab, no focus steal. Pass {stop:true} to clear." })) }  pub fn progress(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {