app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
← Commit history
v1.9.58: badge idle-shed + fusion-taskbar-badge dev skill
8 files changed
+219−12
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.57+1.9.58
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