← Commit history

v1.9.55: driving badge renders via AD-native desktop_taskbar

John Lauer ·4496370079 ·1mo ago ·parent c7c3d33
7 files changed +70−99
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.54\ No newline at end of file+1.9.55\ No newline at end of file
CHANGELOG.md+9
@@ -3,6 +3,15 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge ยท GitHub backup: `adom-inc/fusion-bridge` +## 1.9.55 - 2026-08-17++- **The driving badge actually renders now (pup's lesson, stolen with credit).** In-process+  ITaskbarList3::SetOverlayIcon returns S_OK cross-process and the shell silently ignores it -+  pup root-caused this long ago and routes through AD-core's `desktop_taskbar` verb, which paints+  natively. taskbar_overlay.py now does the same (overlay {icon: b64 png, tooltip} / {badge:+  "none"}). PROVEN live on AdomLapper: red-box capture of Fusion's taskbar button wearing the+  teal Adom mark is in fusion-proofs.+ ## 1.9.54 - 2026-08-17  - **Ralph-test findings on 1.9.53, same day.** (1) An UNATTRIBUTED save prompt (its spawning doc
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {     "manifest_version": 1,     "name": "fusion360",-    "version": "1.9.54",-    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.54/adom-bridge-fusion-v1.9.54.zip",-    "sha256": "b8139e0bc3029fb49e791df6ec9d9b39d2a30f1c6f0163f08ffffaeace2dd660",-    "size": 404402,+    "version": "1.9.55",+    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.55/adom-bridge-fusion-v1.9.55.zip",+    "sha256": "c71cb335452d18c2b129119e30fe999fec5b8475a9d13fd798563a6e395b1533",+    "size": 402467,     "verbPrefixes": [         "fusion_"     ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.54",+  "version": "1.9.55",   "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.",   "homepage": "https://wiki.adom.inc/adom/fusion-bridge",   "author": "Adom Inc.",
handlers/taskbar_overlay.py+54−92
@@ -1,119 +1,81 @@ """Taskbar overlay badge: "Adom is driving Fusion" (John, 2026-08-17). -pup badges its own Chrome taskbar buttons to show state; the same signal belongs on-FUSION's taskbar button whenever this bridge is driving it, so the user can glance at-the taskbar and know why Fusion is busy. Windows exposes exactly this as-ITaskbarList3::SetOverlayIcon - callable cross-process with just an hwnd, no window-ownership needed. ctypes COM, no dependencies.+pup badges its Chrome taskbar buttons so users can tell an Adom-driven window at a+glance; the same signal belongs on FUSION's button whenever this bridge drives it.++THE LESSON (stolen from pup, who paid for it first - src/server.js): an in-bridge+ITaskbarList3::SetOverlayIcon is IN-PROCESS-ONLY. The COM call returns S_OK+cross-process and the shell silently ignores it (verified live here too: badge never+rendered). The working path is AD-core's `desktop_taskbar` verb (AD >=1.9.106): hand+it the badge PNG as base64 and AD paints the overlay natively, cross-process.++    set:   desktop_taskbar {hwnd, overlay: {icon: <b64 png>, tooltip}}+    clear: desktop_taskbar {hwnd, overlay: {badge: "none"}}  Wired to the background envelope's refcount: first envelope (driving starts) badges the-Fusion main window with resources/adom-overlay.ico ("Adom is driving Fusion"); when the-refcount drains to zero the badge clears. Best-effort everywhere - a missing icon, a-COM failure, or a dead hwnd must never break a verb.+Fusion main window with resources/adom-overlay-32.png; when the refcount drains to zero+the badge clears. Best-effort everywhere - a missing asset or an ab error must never+break a verb. PROVEN live on AdomLapper 2026-08-17 (red-box capture in fusion-proofs). """ -import ctypes+import base64 import os import threading  _LOCK = threading.Lock()-_HICON = None+_B64 = None _BADGED_HWND = None -_CLSID_TaskbarList = "{56FDF344-FD6D-11D0-958A-006097C9A090}"-_IID_ITaskbarList3 = "{EA1AFB91-9E28-4B86-90E9-9E9F8A5EEFAF}"-# ITaskbarList3 vtable: IUnknown 0-2 | HrInit 3, AddTab 4, DeleteTab 5, ActivateTab 6,-# SetActiveAlt 7 | MarkFullscreenWindow 8 | SetProgressValue 9, SetProgressState 10,-# RegisterTab 11, UnregisterTab 12, SetTabOrder 13, SetTabActive 14, ThumbBarAddButtons 15,-# ThumbBarUpdateButtons 16, ThumbBarSetImageList 17, SetOverlayIcon 18, ...-_VTBL_HRINIT = 3-_VTBL_SETOVERLAYICON = 18---class _GUID(ctypes.Structure):-    _fields_ = [("Data1", ctypes.c_ulong), ("Data2", ctypes.c_ushort),-                ("Data3", ctypes.c_ushort), ("Data4", ctypes.c_ubyte * 8)]---def _guid(s: str) -> "_GUID":-    g = _GUID()-    ctypes.oledll.ole32.CLSIDFromString(ctypes.c_wchar_p(s), ctypes.byref(g))-    return g --def _icon_path() -> str:-    return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),-                        "resources", "adom-overlay.ico")---def _load_icon():-    global _HICON-    if _HICON:-        return _HICON-    p = _icon_path()-    if not os.path.isfile(p):-        return None-    IMAGE_ICON, LR_LOADFROMFILE = 1, 0x0010-    h = ctypes.windll.user32.LoadImageW(None, p, IMAGE_ICON, 32, 32, LR_LOADFROMFILE)-    _HICON = h or None-    return _HICON+def _badge_b64():+    global _B64+    if _B64 is not None:+        return _B64+    p = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),+                     "resources", "adom-overlay-32.png")+    try:+        _B64 = base64.b64encode(open(p, "rb").read()).decode()+    except Exception:+        _B64 = ""+    return _B64  -def _taskbar_call(hwnd, hicon, description):-    """CoCreate ITaskbarList3 on THIS thread and call SetOverlayIcon once."""-    ole32 = ctypes.oledll.ole32-    ole32.CoInitialize(None)+def _taskbar(args: dict) -> bool:     try:-        clsid = _guid(_CLSID_TaskbarList)-        iid = _guid(_IID_ITaskbarList3)-        ptr = ctypes.c_void_p()-        CLSCTX_INPROC_SERVER = 1-        ole32.CoCreateInstance(ctypes.byref(clsid), None, CLSCTX_INPROC_SERVER,-                               ctypes.byref(iid), ctypes.byref(ptr))-        vtbl = ctypes.cast(ptr, ctypes.POINTER(ctypes.POINTER(ctypes.c_void_p))).contents-        proto_init = ctypes.WINFUNCTYPE(ctypes.HRESULT, ctypes.c_void_p)-        proto_overlay = ctypes.WINFUNCTYPE(ctypes.HRESULT, ctypes.c_void_p,-                                           ctypes.c_void_p, ctypes.c_void_p, ctypes.c_wchar_p)-        proto_release = ctypes.WINFUNCTYPE(ctypes.c_ulong, ctypes.c_void_p)-        hr_init = proto_init(vtbl[_VTBL_HRINIT])(ptr)-        hr = proto_overlay(vtbl[_VTBL_SETOVERLAYICON])(-            ptr, ctypes.c_void_p(hwnd), ctypes.c_void_p(hicon or 0),-            description if description else None)-        proto_release(vtbl[2])(ptr)-        return hr == 0 and hr_init == 0-    finally:-        ole32.CoUninitialize()+        import ad_client+        if not ad_client.available():+            return False+        r = ad_client.call("desktop_taskbar", args, timeout=6)+        raw = str(r)+        return bool(r) and ("Taskbar updated" in raw or (isinstance(r, dict) and not r.get("error")))+    except Exception:+        return False   def set_driving_badge(hwnd) -> bool:     """Badge the window's taskbar button with the Adom mark ('Adom is driving Fusion')."""     global _BADGED_HWND-    try:-        if not hwnd:-            return False-        with _LOCK:-            hicon = _load_icon()-            if not hicon:-                return False-            ok = _taskbar_call(int(hwnd), hicon, "Adom is driving Fusion")-            if ok:-                _BADGED_HWND = int(hwnd)-            return bool(ok)-    except Exception:+    if not hwnd:         return False+    icon = _badge_b64()+    if not icon:+        return False+    with _LOCK:+        ok = _taskbar({"hwnd": int(hwnd),+                       "overlay": {"icon": icon, "tooltip": "Adom is driving Fusion"}})+        if ok:+            _BADGED_HWND = int(hwnd)+        return ok   def clear_driving_badge(hwnd=None) -> bool:     """Remove the badge (from the given hwnd, or whichever window we badged last)."""     global _BADGED_HWND-    try:-        with _LOCK:-            target = int(hwnd) if hwnd else _BADGED_HWND-            if not target:-                return False-            ok = _taskbar_call(target, None, None)-            if ok and (hwnd is None or _BADGED_HWND == int(hwnd)):-                _BADGED_HWND = None-            return bool(ok)-    except Exception:-        return False+    with _LOCK:+        target = int(hwnd) if hwnd else _BADGED_HWND+        if not target:+            return False+        ok = _taskbar({"hwnd": target, "overlay": {"badge": "none"}})+        if ok and (hwnd is None or _BADGED_HWND == int(hwnd)):+            _BADGED_HWND = None+        return ok
resources/adom-overlay-32.pngadded
⋯ 1 unchanged line ⋯
tools/release-files.txt+1−1
@@ -54,4 +54,4 @@ server.py start.bat uninstall.sh handlers/taskbar_overlay.py-resources/adom-overlay.ico+resources/adom-overlay-32.png