← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.18\ No newline at end of file+1.9.19\ No newline at end of file
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {     "manifest_version": 1,     "name": "fusion360",-    "version": "1.9.18",-    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.18/adom-bridge-fusion-v1.9.18.zip",-    "sha256": "1eaa480c6c410aa41143e01dd2edcb580093a38dcbf1d093ff2c70632c95e558",-    "size": 360521,+    "version": "1.9.19",+    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.19/adom-bridge-fusion-v1.9.19.zip",+    "sha256": "88cc7d1c6bdb87b84a5e4f51672221a3eee2ae83f5162d86dcbc107f7ccafe95",+    "size": 361294,     "verbPrefixes": [         "fusion_"     ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.18",+  "version": "1.9.19",   "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-background-etiquette/SKILL.md+13−5
@@ -28,9 +28,14 @@ foreground mid-keystroke is the single most user-hostile thing this bridge can d 1. **API / text command** - the add-in API, EAGLE text commands, and modeling scripts    need no window at all. Most "UI" work is really this. 2. **Background window driving** - hwnd-targeted PrintWindow capture, UI Automation-   (find_control / ui_click / ui_set), and SendMessage-style input all work on windows-   at the BOTTOM of the z-order. A thing being "in the UI" does not make it a-   foreground thing.+   (find_control / ui_click / ui_set), and PostMessage/SendMessage input all work on+   windows at the BOTTOM of the z-order. A thing being "in the UI" does not make it a+   foreground thing. The KiCad bridge proved the strong form (John, 2026-08-15): it+   reads the app's REAL pulldown menus and drives them with PostCommand/WM_COMMAND+   instead of foreground+SendInput, "and now kicad works better." Our equivalents:+   `send_key_background` (PostMessage WM_KEYDOWN/UP, the DEFAULT for fusion_send_key+   since 1.9.19) and `_dismiss_dialogs_bg` (WM_CLOSE). SendInput is the explicit,+   captioned exception (`method: "sendinput"`), never the default. 3. **Background + demotion envelope** - if the operation makes Fusion raise windows or    dialogs, run it inside the envelope so every grab is denied/demoted (incl. the    drain sweep below).@@ -57,8 +62,11 @@ before trusting its exception status. overlapping verbs nest and only the last drain restores the user's settings. It does three things for its duration: -1. Raises the Windows foreground-lock timeout, so focus steals are DENIED up front and-   become taskbar flashes (this kills the visible flicker; the watcher alone cannot).+1. DENIES grabs up front: raises the foreground-lock timeout AND calls+   LockSetForegroundWindow for the envelope's duration (Fusion self-activates on every+   document/workspace switch; before the lock, each one FLASHED for the instant before+   the watcher demoted it - John counted three in one proof run, 2026-08-15). Both are+   best-effort; the watcher below is the backstop that always works. 2. Disables the minimize animation, so any demotion is an instant vanish. 3. Runs a watcher: any Autodesk-FAMILY window that still reaches the foreground is    demoted. Two modes:
fusion_detect.py+14
@@ -743,6 +743,15 @@ def begin_background_envelope(seconds: float, minimize: bool = False):                 _ENVELOPE_SAVED["anim"] = _get_min_animate()                 _set_fg_lock_timeout(300000)                 _set_min_animate(0)+                # DENY grabs outright, don't just undo them: Fusion self-activates on+                # document/workspace switches and each one FLASHED before the watcher+                # demoted it (John counted three, 2026-08-15). LockSetForegroundWindow+                # blocks SetForegroundWindow globally until unlock; best-effort (Windows+                # only honors it from some process states) - watcher stays as backstop.+                try:+                    _ENVELOPE_SAVED["lsfw"] = bool(ctypes.windll.user32.LockSetForegroundWindow(1))+                except Exception:+                    _ENVELOPE_SAVED["lsfw"] = False         threading.Thread(target=_envelope_watch, args=(seconds, minimize), daemon=True).start()     except Exception:         pass@@ -812,6 +821,11 @@ def _envelope_watch(seconds: float, minimize: bool):                     _set_fg_lock_timeout(_ENVELOPE_SAVED["fg"])                 if _ENVELOPE_SAVED.get("anim") is not None:                     _set_min_animate(_ENVELOPE_SAVED["anim"])+                if _ENVELOPE_SAVED.get("lsfw"):+                    try:+                        ctypes.windll.user32.LockSetForegroundWindow(2)+                    except Exception:+                        pass   def _focus_guard(seconds: float = 150.0, restore_fg_lock=None, restore_min_anim=None):
handlers/fusion_ui.py+28
@@ -628,6 +628,34 @@ def click_fusion(x, y, relative=True, hwnd=None) -> dict:  # --- Keyboard --- +def send_key_background(key: str, hwnd=None) -> dict:+    """Send a key via PostMessage - NO foreground, no focus steal, the KiCad-bridge+    lesson (John, 2026-08-15: kicad reads real menus + PostCommand instead of+    foreground+SendInput and 'now kicad works better'). Works for the dialog keys that+    matter (escape/enter/tab) on Win32 and most Qt dialogs; SendInput stays as the+    explicit, captioned last resort for CEF surfaces that ignore posted input.+    """+    if hwnd:+        hwnd = int(hwnd)+        if not user32.IsWindow(hwnd):+            return {"success": False, "error": f"HWND {hwnd} is not a valid window."}+    else:+        hwnd = _find_fusion_hwnd()+        if not hwnd:+            return {"success": False, "error": "Fusion 360 window not found."}+    vk = VK_MAP.get(key.lower().strip())+    if vk is None and len(key) == 1:+        vk = user32.VkKeyScanW(ord(key)) & 0xFF+    if vk in (None, 0xFF):+        return {"success": False, "error": f"Cannot map key '{key}' for the background path."}+    WM_KEYDOWN, WM_KEYUP = 0x0100, 0x0101+    ok1 = user32.PostMessageW(hwnd, WM_KEYDOWN, vk, 0x00000001)+    ok2 = user32.PostMessageW(hwnd, WM_KEYUP, vk, 0xC0000001)+    if not (ok1 and ok2):+        return {"success": False, "error": "PostMessage refused the key (window gone or access denied)."}+    return {"success": True, "key": key, "vk": vk, "method": "postmessage-background"}++ def send_key_to_fusion(key: str, hwnd=None) -> dict:     """Send a key to Fusion 360 (or a specific dialog) via SendInput. 
server.py+11−4
@@ -30,6 +30,7 @@ from handlers.open_design import handle_open_design from handlers.close_fusion import handle_close_fusion, handle_fusion_stop, handle_fusion_kill from handlers.dismiss_recovery import dismiss_recovery_dialog from handlers.fusion_ui import (+    send_key_background,     screenshot_fusion_window,     screenshot_hwnd,     click_fusion,@@ -1059,14 +1060,20 @@ def _handle_click_fusion(fusion_info: dict, args: dict) -> dict:   def _handle_send_key(fusion_info: dict, args: dict) -> dict:-    """Send a key to Fusion or a specific dialog via SendInput."""+    """Send a key to Fusion or a dialog - BACKGROUND PostMessage first (the KiCad-bridge+    lesson: real window messages beat foreground+SendInput); SendInput only on explicit+    {"method": "sendinput"} or when the posted key visibly failed, and then ALWAYS with+    the user-facing caption explaining WHY (John's rule, 2026-08-15)."""     key = args.get("key", "")     if not key:         return {"success": False, "error": "No key specified. Use 'enter', 'escape', 'tab', etc."}     hwnd = args.get("hwnd") or args.get("dialogHwnd")-    # SendInput keystrokes REQUIRE foreground (steals the user's focus). Announce it first --    # prefer background verbs (desktop_ui_set / WM_CLOSE); this path is the last resort.-    _notify_before_foreground(args.get("reason") or f"sending the '{key}' key, which Fusion only accepts in the foreground")+    if args.get("method") != "sendinput":+        bg = send_key_background(key, hwnd=hwnd)+        if bg.get("success"):+            bg["output"] = f"Sent key '{key}' via background PostMessage (no focus taken)."+            return bg+    _notify_before_foreground(args.get("reason") or f"sending the '{key}' key, which this surface only accepts in the foreground")     result = send_key_to_fusion(key, hwnd=hwnd)     if result.get("success"):         result["output"] = f"Sent key '{key}' to Fusion."