← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.57\ No newline at end of file+1.9.58\ No newline at end of file
CHANGELOG.md+12
@@ -3,6 +3,18 @@ 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.58 - 2026-08-17++- **Driving badge sheds on idle, nb-style (John's call).** Every non-passive verb stamps a+  driving-activity clock (passive reads - readiness/state/demo_progress polls etc. - are+  excluded, or the Dashboard's 15s polls would pin the badge forever); the 20s janitor clears+  the badge after 5 minutes of quiet. No more per-verb on/off flicker (the envelope-drain clear+  is gone). Badge set is throttled (60s/same-hwnd) and async so it never slows a verb.+  fusion_state now reports drivingBadge {active, lastDrivingVerbAt, idleShedSeconds}.+- **dev-skills/fusion-taskbar-badge**: the complete verbose recipe (SetOverlayIcon trap,+  desktop_taskbar contract, brand asset provenance + sizing, activity/idle lifecycle, red-box+  proof method, kicad porting checklist).+ ## 1.9.57 - 2026-08-17  - **Driving badge shrunk ~30%** (John: the user should see MORE of the Fusion icon and only
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {     "manifest_version": 1,     "name": "fusion360",-    "version": "1.9.57",-    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.57/adom-bridge-fusion-v1.9.57.zip",-    "sha256": "459aee266a738996f5c4430fe16a53a270f1b8823eda7574a9b4c72ed28c12ca",-    "size": 402564,+    "version": "1.9.58",+    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.58/adom-bridge-fusion-v1.9.58.zip",+    "sha256": "ce08bd435735759f758f11b07f90185cf54faa8e1aecc249b4dfea21d1eb6c5a",+    "size": 403808,     "verbPrefixes": [         "fusion_"     ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.57",+  "version": "1.9.58",   "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.",
dev-skills/fusion-taskbar-badge/SKILL.mdadded+125
@@ -0,0 +1,125 @@+---+name: fusion-taskbar-badge+description: The "Adom is driving Fusion" taskbar overlay badge - the COMPLETE recipe (asset, cross-process painting via ab desktop_taskbar, activity-tracked idle shedding, proof method) written verbosely so kicad-bridge (and every future app bridge) can copy it verb-for-verb. Read BEFORE touching handlers/taskbar_overlay.py or porting the badge to another bridge.+---++# fusion-taskbar-badge - the driving badge, end to end++## What it is and why (John, 2026-08-17, verbatim intents)++> "pup places overlay icons over my windows taskbar icons to indicate lots of state. for+> you, it would seem it would make sense to apply an overlay icon of the adom favicon to+> indicate to an adom user that 'adom is driving my fusion app'. i think that would be+> super rad."++> "make that adom favicon smaller, like 30% smaller, so the adom user sees more of the+> fusion 360 windows taskbar icon and just a small version of the adom favicon so they+> know 'slightly' that we are driving it"++> "after we're done controlling fusion, u should probably shed the active adom overlay+> icon badge like the way nb does it after like 5 or 10 minutes of inactivity"++The badge is a SMALL Adom brand mark on the corner of the app's Windows taskbar button,+visible only while Adom is actively driving the app. It answers, at a glance, "why is+Fusion busy?" - and disappears once Adom has been quiet for a while, so a lingering badge+never lies about who is in control.++## ⚠ THE TRAP that costs you a day: SetOverlayIcon is in-process-only++`ITaskbarList3::SetOverlayIcon` called from YOUR process on ANOTHER process's window+**returns S_OK and the shell silently ignores it**. No error, no badge. pup paid for+this lesson first (src/server.js: "an in-bridge SetOverlayIcon is in-process-only and+fails on the browser's out-of-process window") and we re-verified it here: a full ctypes+COM implementation, HRESULT 0, nothing rendered. Do NOT reimplement COM. Delete any+attempt you find.++**The working path: AD-core's `desktop_taskbar` verb (AD >= 1.9.106).** ab paints the+overlay natively, cross-process. The bridge just hands over the art:++```jsonc+// set (icon = base64 PNG, 32x32 canvas):+desktop_taskbar {"hwnd": <app main hwnd>, "overlay": {"icon": "<b64>", "tooltip": "Adom is driving Fusion"}}+// clear:+desktop_taskbar {"hwnd": <hwnd>, "overlay": {"badge": "none"}}+```++Success check: the response's output contains "Taskbar updated". The fusion+implementation calls it through `ad_client.call("desktop_taskbar", args, timeout=6)`+(the AD direct API; from a bridge process, no target arg = this machine).++## The asset (brand compliance - John rejected an improvised glyph)++- Source of truth: **pup's canonical overlay** `src/icons/pup-overlay-32.png` on+  adom/pup-bridge - the teal rounded square with the two dark Adom leaves. That IS the+  Adom favicon look; do not draw your own mark (the first attempt here was a white "a"+  on teal and John bounced it: "that overlay is not the adom favicon").+- Sizing: render the mark at **22x22 inside a TRANSPARENT 32x32 canvas, pasted at+  (10,10)** so it hugs the overlay slot's corner. This is John's "30% smaller" ruling:+  the app's own icon stays readable and the mark only *whispers* that Adom is driving.+  Regeneration snippet (PIL, run in the container):++```python+from PIL import Image+src = Image.open("pup-overlay-32.png").convert("RGBA")+mark = src.resize((22, 22), Image.LANCZOS)+canvas = Image.new("RGBA", (32, 32), (0, 0, 0, 0))+canvas.paste(mark, (10, 10), mark)+canvas.save("resources/adom-overlay-32.png")+```++- Ship the PNG in the bridge zip (`resources/adom-overlay-32.png`, listed in+  tools/release-files.txt). The helper base64s it once and caches.++## Lifecycle: activity-tracked, idle-shed (v1.9.58)++Three moving parts, all in server.py + handlers/taskbar_overlay.py:++1. **ON - every DRIVING verb.** `dispatch_command` calls `_note_driving_activity(command)`+   right after the prefix strip. It stamps `_LAST_DRIVE_AT` and fires a daemon thread that+   badges the app's main hwnd (found by largest-family-window). The set helper is+   THROTTLED (same hwnd within 60s = no-op) so per-verb calls never spam desktop_taskbar,+   and it runs async because a desktop_taskbar call can take seconds and must never slow+   a verb.+2. **PASSIVE verbs are excluded - this is the subtle part.** `_BADGE_PASSIVE` lists pure+   reads: readiness, state, window_info, describe, demo_progress, aps/mcp status, pref+   reads, screenshots, app-state reads. WHY: the Dashboard polls readiness every 15s and+   tour pollers hit demo_progress every ~2s - if those counted as "driving," the badge+   would NEVER shed. Driving = verbs that change or command the app, not verbs that look+   at it. When you add a new read-only verb, ADD IT TO THE PASSIVE SET.+3. **OFF - idle timeout in the janitor.** The 20s janitor tick checks+   `badge_active() and now - _LAST_DRIVE_AT > _BADGE_IDLE_SECONDS` (300s; John said+   "5 or 10 minutes", five keeps the taskbar honest sooner) and clears via+   `overlay:{badge:"none"}`. The badge is NOT cleared on envelope drain anymore - that+   produced per-verb on/off flicker, which reads as noise, not state.++Also badged at background-envelope BEGIN (fusion_detect) - harmless double-set thanks to+the throttle, and it catches the window the instant a window-raising verb starts.++Observability: `fusion_state` returns `drivingBadge: {active, lastDrivingVerbAt,+idleShedSeconds}` so an AI (or the daily suite) can assert the lifecycle.++## Proof method (how to SHOW the badge working - John's requirement)++A page/window screenshot never shows the taskbar. The proof recipe:+1. `desktop_screenshot_screen {}` via ab - the **`.full.png`** artifact carries the real+   desktop including the taskbar (the `.safe.png` is downscaled; use full).+2. Crop the bottom ~56px strip, find the app's icon (color-mask scan works: Autodesk+   orange = r>200, 60<g<160, b<80, constrained to the expected x-region), draw a RED BOX+   around it, upscale 3x, shotlog it.+3. Caveat: a box with taskbar AUTO-HIDE (ConfRoomROG) shows nothing - the strip reads as+   the app's own pixels. Verify on a box with a visible taskbar.++## Porting checklist for kicad-bridge++- [ ] Copy `handlers/taskbar_overlay.py` nearly verbatim; swap tooltip text+      ("Adom is driving KiCad") and the resources path.+- [ ] Use the SAME asset recipe (pup's mark at 22px in a 32px transparent canvas) -+      users should learn ONE badge across every Adom-driven app.+- [ ] Hook `_note_driving_activity` into your dispatch; build YOUR passive set (remember+      kicad_state and any web-demo pollers - the same never-sheds trap applies).+- [ ] Idle-shed from whatever periodic loop you already run (fusion uses its 20s janitor).+- [ ] kicad already has the always-on self-raise sentinel + focusEvents - consider logging+      badge set/shed into focusEvents so quietness AND badge lifecycle are assertable.+- [ ] Multi-window apps: badge the MAIN window's button; on a grouped taskbar button+      Windows shows the most recently updated window's overlay (pup's note).+- [ ] Prove with the red-box method before claiming it works. SetOverlayIcon lies.
fusion_detect.py+3−5
@@ -863,11 +863,9 @@ def _envelope_watch(seconds: float, minimize: bool):                         ctypes.windll.user32.LockSetForegroundWindow(2)                     except Exception:                         pass-                try:-                    from handlers.taskbar_overlay import clear_driving_badge-                    clear_driving_badge()-                except Exception:-                    pass+                # v1.9.58: badge is NO LONGER cleared on envelope drain - it sheds on verb+                # IDLE instead (server's janitor, John: "after like 5 or 10 minutes of+                # inactivity, the way nb does it"). Per-verb on/off flicker was wrong.   def _focus_guard(seconds: float = 150.0, restore_fg_lock=None, restore_min_anim=None):
handlers/taskbar_overlay.py+14−1
@@ -52,11 +52,19 @@ def _taskbar(args: dict) -> bool:         return False  +_LAST_SET = [0.0]++ def set_driving_badge(hwnd) -> bool:-    """Badge the window's taskbar button with the Adom mark ('Adom is driving Fusion')."""+    """Badge the window's taskbar button with the Adom mark ('Adom is driving Fusion').+    Throttled: re-sending the same badge to the same hwnd within 60s is a no-op, so+    per-verb callers do not spam desktop_taskbar."""     global _BADGED_HWND     if not hwnd:         return False+    import time+    if _BADGED_HWND == int(hwnd) and (time.time() - _LAST_SET[0]) < 60:+        return True     icon = _badge_b64()     if not icon:         return False@@ -65,9 +73,14 @@ def set_driving_badge(hwnd) -> bool:                        "overlay": {"icon": icon, "tooltip": "Adom is driving Fusion"}})         if ok:             _BADGED_HWND = int(hwnd)+            _LAST_SET[0] = time.time()         return ok  +def badge_active() -> bool:+    return _BADGED_HWND is not None++ def clear_driving_badge(hwnd=None) -> bool:     """Remove the badge (from the given hwnd, or whichever window we badged last)."""     global _BADGED_HWND
server.py+59
@@ -4502,6 +4502,13 @@ def _handle_fusion_state(fusion_info: dict, args: dict) -> dict:     except Exception:         result["addinResponding"] = False     result["mainThreadBusy"] = busy+    try:+        from handlers.taskbar_overlay import badge_active+        result["drivingBadge"] = {"active": badge_active(),+                                  "lastDrivingVerbAt": _LAST_DRIVE_AT[0] or None,+                                  "idleShedSeconds": _BADGE_IDLE_SECONDS}+    except Exception:+        pass     # Active document, best-effort (skip when busy - it would just be rejected).     if result.get("addinResponding") and not busy:         try:@@ -5112,6 +5119,43 @@ def _signin_goal_met(kind) -> bool:  _last_seat_takeover_notice = [0.0] +# ── Driving badge lifecycle (John, 2026-08-17: "after we're done controlling fusion, shed+# the badge like nb does - after like 5 or 10 minutes of inactivity") ────────────────────+# Badge ON: any DRIVING verb dispatch while Fusion runs (set here, throttled in the helper).+# Badge OFF: the janitor sheds it once no driving verb has run for _BADGE_IDLE_SECONDS.+# PASSIVE reads are excluded from "driving" ON PURPOSE: the Dashboard polls readiness every+# 15s and pollers poll demo_progress every ~2s - counting those would pin the badge forever.+_BADGE_IDLE_SECONDS = 300   # John said 5-10 min; 5 keeps the taskbar honest sooner+_LAST_DRIVE_AT = [0.0]+_BADGE_PASSIVE = {+    "readiness", "state", "window_info", "describe", "demo_progress", "addin_status",+    "aps_status", "mcp_status", "get_preferences", "targets", "screenshot_fusion",+    "screenshot_all", "check_dialogs", "set_auto_update", "set_seat_takeover",+    "uninstall", "get_app_state", "board_info", "document_info",+}+++def _note_driving_activity(command: str):+    """Called from dispatch for every verb: driving verbs stamp the activity clock and+    ensure the badge is on (async - a 6s desktop_taskbar call must never slow a verb)."""+    if command in _BADGE_PASSIVE:+        return+    import time as _t+    _LAST_DRIVE_AT[0] = _t.time()++    def _ensure():+        try:+            from handlers.taskbar_overlay import set_driving_badge+            from fusion_detect import _fusion_main_hwnd_quick+            if _is_fusion_running():+                hw = _fusion_main_hwnd_quick()+                if hw:+                    set_driving_badge(hw)+        except Exception:+            pass+    threading.Thread(target=_ensure, daemon=True).start()++  def _seat_takeover_enabled() -> bool:     """Default TRUE (John: "always just take over the session"). Sticky opt-out for users@@ -5186,6 +5230,14 @@ def _seat_sweep_tick():                   % (r.get("clicks"), r.get("verified")))     except Exception:         pass+    # Shed the driving badge after idle (John: "so the user knows we are DONE controlling").+    try:+        from handlers.taskbar_overlay import badge_active, clear_driving_badge+        if badge_active() and _LAST_DRIVE_AT[0] and (_t2.time() - _LAST_DRIVE_AT[0]) > _BADGE_IDLE_SECONDS:+            if clear_driving_badge():+                print("[Fusion Bridge] driving badge shed after %ds idle" % _BADGE_IDLE_SECONDS)+    except Exception:+        pass     # Same tick also sweeps a standing Save prompt for BRIDGE-CREATED docs (2026-08-16:     # a bare "Save" Qt modal from a killed session blocked the main thread invisibly and     # failed two tours in a row - the sentinel only ran during doc-switch calls).@@ -7751,6 +7803,13 @@ def dispatch_command(command: str, args: dict, caller_identity: dict = None) ->     if command.startswith("fusion_"):         command = command[7:] +    # Driving-badge activity clock (John, 2026-08-17): every non-passive verb marks Fusion+    # as actively driven; the janitor sheds the badge after _BADGE_IDLE_SECONDS of quiet.+    try:+        _note_driving_activity(command)+    except Exception:+        pass+     # Document guard (issue #289 items 1 AND 5, Oliver): a verb carrying `expectDocument` must not     # run against a document the caller didn't intend. Originally only mutating verbs were guarded;     # Oliver's BOM session proved READS need it too - a passive tab switch silently returned a