← Commit history

0.2.1: a finished verb can no longer leave a stale partial progress bar on Altium's taskbar icon — the terminal clear is painted by the ticker thread itself (paints are sequential there), so an in-flight tick can never land after it; John caught it minutes after the swap. Also release.py's repo push uses the --files form.

John Lauer ·ca58f6dddc ·1mo ago ·parent 40eee2b
10 files changed +55−33
CHANGELOG.md+4
@@ -1,5 +1,9 @@ # Changelog +## 0.2.1 — 2026-09-02+- **A finished verb can no longer leave a stale partial progress bar on Altium's taskbar icon.** John spotted it minutes after the 0.2.0 swap. Root cause: every progress paint is an ab round trip, and the clear was painted from the dispatch thread — so a tick still in flight from the ticker thread could land AFTER the clear and park a "47%" frame on the icon forever (a race the Python bridge carried latently; the Rust timing exposed it). The terminal frames (error flash, clear, flash:stop) are now painted by the ticker thread itself, where paints are sequential, so the clear is guaranteed to be last; the ticker also sleeps in 100 ms slices so the clear lands promptly.++ ## 0.2.0 — 2026-09-02 - **The bridge is Rust, end to end.** John: *"let's finish getting everything into the rust code. i don't like that we're half rust half python now."* All 112 verbs now dispatch from `bridge-rs` (`altium-bridge.exe`, `spawn.kind: "exe"` per ab's SDK); the Python runtime no longer ships. What crossed over, at behavioural parity with 0.1.41:   - the **instance layer** (`instance:<pid|key|word>`, `ambiguous_instance` refusal, per-instance agent workspaces `agent/<key>`, instance-aware licence gate incl. the Develop absent-tail rule);
adom-bridge-altium-manifest.json+4−4
@@ -1,9 +1,9 @@ {   "name": "altium",-  "version": "0.2.0",-  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.2.0/adom-bridge-altium-v0.2.0.zip",-  "sha256": "2c425b77f908edd849829b2a36153f9d9a8e17363eed7e1728e0a70cb93ce992",-  "size": 1403462,+  "version": "0.2.1",+  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.2.1/adom-bridge-altium-v0.2.1.zip",+  "sha256": "dfba9019daa194f1c7366ed8bb782f7477705856f691ccad35f326bb5652556c",+  "size": 1403871,   "verbPrefixes": [     "altium_"   ],
bridge-rs/Cargo.lock+1−1
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "altium-bridge"-version = "0.2.0"+version = "0.2.1" dependencies = [  "getrandom",  "serde",
bridge-rs/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "altium-bridge"-version = "0.2.0"+version = "0.2.1" edition = "2021" description = "Adom altium-bridge — the bridge process, in Rust. Calls ab's verbs for the desktop; speaks file IPC to the resident DelphiScript agent inside Altium." license = "MIT"
bridge-rs/src/main.rs+1−1
@@ -24,7 +24,7 @@ mod verbs; use std::time::Instant; use tiny_http::{Header, Method, Response, Server}; -pub const BRIDGE_VERSION: &str = "0.2.0";+pub const BRIDGE_VERSION: &str = "0.2.1";  fn json_response(status: u16, body: &serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> {     let data = serde_json::to_vec(body).unwrap_or_else(|_| b"{}".to_vec());
bridge-rs/src/taskbar.rs+36−22
@@ -15,7 +15,12 @@ use std::time::Duration; static BADGED: Mutex<BTreeSet<i64>> = Mutex::new(BTreeSet::new()); static LAST_DRIVING: AtomicI64 = AtomicI64::new(0); static IDLE_WATCHER: AtomicBool = AtomicBool::new(false);-static PROGRESS_STOP: Mutex<BTreeMap<String, std::sync::Arc<AtomicBool>>> = Mutex::new(BTreeMap::new());+struct ProgressHandle {+    stop: std::sync::Arc<AtomicBool>,+    failed: std::sync::Arc<AtomicBool>,+}++static PROGRESS_STOP: Mutex<BTreeMap<String, ProgressHandle>> = Mutex::new(BTreeMap::new());  pub const IDLE_CLEAR_SEC: i64 = 300; @@ -130,10 +135,17 @@ fn taskbar_progress(state: &str, value: Option<i64>) -> bool { }  /// Begin a progress bar for `verb`, paced by its measured estimate (marquee when none).+///+/// The FINAL clear is painted by this ticker thread, not by `end_activity_progress`: every paint+/// is an ab round trip, so a clear painted from another thread can lose the race to a tick still+/// in flight — leaving a stale partial bar parked on Altium's taskbar icon forever (John saw+/// exactly that, 2026-09-02, minutes after the Rust swap). Paints are sequential within this+/// thread, so a clear IT paints is guaranteed to be last. pub fn start_activity_progress(verb: &str, estimate_s: Option<f64>, confidence: &str) {     let stop = std::sync::Arc::new(AtomicBool::new(false));+    let failed = std::sync::Arc::new(AtomicBool::new(false));     if let Ok(mut g) = PROGRESS_STOP.lock() {-        g.insert(verb.to_string(), stop.clone());+        g.insert(verb.to_string(), ProgressHandle { stop: stop.clone(), failed: failed.clone() });     }     let est = estimate_s.filter(|e| *e > 0.0 && confidence != "unknown");     let _ = std::thread::Builder::new().name(format!("altium-progress-{verb}")).spawn(move || {@@ -142,15 +154,18 @@ pub fn start_activity_progress(verb: &str, estimate_s: Option<f64>, confidence:             None => {                 taskbar_progress("indeterminate", None);                 while !stop.load(Ordering::Relaxed) {-                    std::thread::sleep(Duration::from_secs(1));+                    std::thread::sleep(Duration::from_millis(250));                 }             }             Some(e) => {                 let mut fails = 0u32;-                while !stop.load(Ordering::Relaxed) {-                    std::thread::sleep(Duration::from_millis(700));-                    if stop.load(Ordering::Relaxed) {-                        break;+                'ticks: while !stop.load(Ordering::Relaxed) {+                    // sleep in small slices so the final clear lands promptly after the verb ends+                    for _ in 0..7 {+                        std::thread::sleep(Duration::from_millis(100));+                        if stop.load(Ordering::Relaxed) {+                            break 'ticks;+                        }                     }                     let pct = ((95.0 * started.elapsed().as_secs_f64() / e) as i64).min(95);                     if taskbar_progress("normal", Some(pct)) {@@ -158,30 +173,29 @@ pub fn start_activity_progress(verb: &str, estimate_s: Option<f64>, confidence:                     } else {                         fails += 1; // window may not exist yet during a cold launch — keep trying                         if fails > 120 {-                            return;+                            break; // still fall through to the clear below                         }                     }                 }             }         }+        // terminal frames, from THIS thread only+        if failed.load(Ordering::Relaxed) {+            taskbar_progress("error", Some(100));+            std::thread::sleep(Duration::from_millis(1200));+        }+        taskbar_progress("none", None);+        if let Some(h) = main_hwnd() {+            // cancel Windows' own orange attention flash (John: "can u not orange flash the taskbar")+            taskbar(json!({"hwnd": h, "flash": "stop"}));+        }     }); }  pub fn end_activity_progress(verb: &str, failed: bool) {-    let stop = PROGRESS_STOP.lock().ok().and_then(|mut g| g.remove(verb));-    if let Some(s) = stop {-        s.store(true, Ordering::Relaxed);-    } else {-        return; // no bar was started for this verb-    }-    if failed {-        taskbar_progress("error", Some(100));-        std::thread::sleep(Duration::from_millis(1200));-    }-    taskbar_progress("none", None);-    if let Some(h) = main_hwnd() {-        // cancel Windows' own orange attention flash (John: "can u not orange flash the taskbar")-        taskbar(json!({"hwnd": h, "flash": "stop"}));+    if let Some(h) = PROGRESS_STOP.lock().ok().and_then(|mut g| g.remove(verb)) {+        h.failed.store(failed, Ordering::Relaxed);+        h.stop.store(true, Ordering::Relaxed); // the ticker paints the clear (see above)     } } 
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "altium-bridge",-  "version": "0.2.0",+  "version": "0.2.1",   "type": "app",   "description": "Skills for your container so your AI knows how to drive the Altium Designer bridge (altium_* verbs). The bridge runtime itself is the release zip; Adom Bridge loads that.",   "tags": [
scripts/release.py+5−1
@@ -118,7 +118,11 @@ def main():         run = lambda *a: subprocess.run(a, check=True)         run("adom-wiki", "release", "create", PAGE, ver, "--title", f"altium-bridge {ver}", "--changelog", "see CHANGELOG.md")         run("adom-wiki", "release", "upload", PAGE, ver, str(zpath), "--platform", "windows", "--arch", "x64")-        run("adom-wiki", "repo", "push", PAGE)   # manifest + page files+        # `repo push` requires --files now; the version stamps travel with the manifest so the+        # page repo can never disagree with the release it serves+        run("adom-wiki", "repo", "push", PAGE, "-m", f"release {ver}: manifest + version stamps",+            "--files", "adom-bridge-altium-manifest.json", "CHANGELOG.md", "src/bridge.json",+            "src/BRIDGE_VERSION", "package.json")         print("published")  
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.2.0+0.2.1
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "altium",   "displayName": "Altium Designer",-  "version": "0.2.0",+  "version": "0.2.1",   "description": "Drive Altium Designer from Adom: a resident DelphiScript agent (eval + the 1,100-command process launcher), the X2 -R command line, UIA over the VCL shell, dialogs by PID, screenshots of every Altium window, and Altium 365.",   "homepage": "https://wiki.adom.inc/adom/altium-bridge",   "docs": "https://wiki.adom.inc/adom/altium-bridge",