← Commit history

v1.6.33: launch Fusion in background (minimized+no-activate+focus-guard); dev-skill invariant

John Lauer ·aa89fbec29 ·3mo ago ·parent 39e44b4
6 files changed +146−18
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.6.32\ No newline at end of file+1.6.33\ No newline at end of file
CHANGELOG.md+12
@@ -3,6 +3,18 @@ All notable changes to the Adom Desktop Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/adom-desktop-fusion-bridge · GitHub backup: `adom-inc/adom-desktop-fusion-bridge` +## 1.6.33 - 2026-07-06++- **Fusion now launches in the BACKGROUND - never steals the user's foreground** (John's directive).+  The bridge runs on a machine the user is actively working on, and a bare `Popen([launcher])` opened+  Fusion's splash + main window in the foreground, yanking focus off their work. Now `ensure_fusion_+  running()` starts the launcher MINIMIZED + NOT ACTIVATED (`STARTUPINFO SW_SHOWMINNOACTIVE`) and fires+  a ~120s **focus-guard** daemon that minimizes (no-activate) *only* a Fusion window that is *currently*+  the foreground (focus-theft detected via `GetForegroundWindow` + PID match, so it never fights a user+  who deliberately opened Fusion). The add-in still loads fully while minimized. Foreground is now+  opt-in, only when a verb's job is to SHOW the user. Codified as a HARD invariant in the+  `fusion-bridge-dev` skill so every future window-opening path follows it.+ ## 1.6.32 - 2026-07-06  - **Fixed a false positive in 1.6.31's `updating` detection** (caught immediately on a real laptop
adom-bridge-fusion-manifest.json+5−5
@@ -1,13 +1,13 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.6.32",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.32/adom-bridge-fusion-v1.6.32.zip",-  "sha256": "daaf252426ff17ee0a327ce003cabe704d525b871f0fefe90f1969c7da184086",-  "size": 330230,+  "version": "1.6.33",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.33/adom-bridge-fusion-v1.6.33.zip",+  "sha256": "70b10a6c7eabfa5116fa749a1d02b76fc5b918787fcf64b5387da567f317bc65",+  "size": 332961,   "verbPrefixes": [     "fusion_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-07-06T10:51:48Z"+  "released_at": "2026-07-06T11:01:29Z" }
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.6.32",+  "version": "1.6.33",   "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/adom-desktop-fusion-bridge",   "author": "Adom Inc.",
fusion_detect.py+98−11
@@ -4,7 +4,9 @@ import ctypes import ctypes.wintypes import json import os+import re import subprocess+import threading import time import urllib.error import urllib.request@@ -283,6 +285,94 @@ def _dismiss_startup_dialogs():         pass  +# ── Background launch: NEVER steal the user's foreground ──────────────────────+# The bridge launches Fusion for AUTOMATION, on a machine the user is actively+# working on. Fusion normally opens its splash + main window in the FOREGROUND,+# yanking focus off whatever the user is doing. That is unacceptable (John, 2026-+# 07-06: "make sure new windows ALWAYS only open in the background"). So we (1)+# start the process MINIMIZED + NOT ACTIVATED via STARTUPINFO, and (2) run a short+# focus-guard thread that, whenever a Fusion window steals the foreground during+# the noisy startup window, minimizes it WITHOUT activating - returning focus to+# the user's work. The add-in still loads fully while minimized. To SHOW Fusion to+# the user later, a verb explicitly restores+foregrounds it (the ONLY time we ever+# foreground). See the fusion-bridge-dev skill "Background-launch invariant".+_SW_SHOWMINNOACTIVE = 7  # show minimized, do NOT activate/steal focus+++def _fusion_pids() -> set:+    """PIDs of running Fusion360.exe processes (via tasklist CSV). Empty on error."""+    try:+        cnw = subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0+        out = subprocess.check_output(+            ["tasklist", "/FI", "IMAGENAME eq Fusion360.exe", "/FO", "CSV", "/NH"],+            stdin=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=cnw,+        ).decode("utf-8", "replace")+    except Exception:+        return set()+    return {int(p) for p in re.findall(r'"Fusion360\.exe","(\d+)"', out)}+++def _popen_fusion_background(exe_path: str):+    """Launch the Fusion launcher MINIMIZED + NOT ACTIVATED, console-less."""+    cnw = subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0+    startupinfo = None+    try:+        startupinfo = subprocess.STARTUPINFO()+        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW+        startupinfo.wShowWindow = _SW_SHOWMINNOACTIVE+    except Exception:+        startupinfo = None+    return subprocess.Popen(+        [exe_path],+        stdin=subprocess.DEVNULL,+        stdout=subprocess.DEVNULL,+        stderr=subprocess.DEVNULL,+        creationflags=cnw,+        startupinfo=startupinfo,+    )+++def _focus_guard(seconds: float = 120.0):+    """For `seconds`, demote any Fusion window that steals the foreground.++    SURGICAL: only acts when the CURRENT foreground window belongs to a Fusion+    process (i.e. Fusion just grabbed focus). It minimizes that window without+    activating anything, so focus falls back to whatever the user was using. It+    does NOT touch Fusion when the user is in another app, so it never fights a+    user who deliberately opened Fusion. Best-effort; never raises."""+    try:+        user32 = ctypes.windll.user32+    except Exception:+        return+    end = time.time() + seconds+    pids = set()+    last_refresh = 0.0+    while time.time() < end:+        now = time.time()+        if now - last_refresh > 3.0:+            pids = _fusion_pids()+            last_refresh = now+        try:+            fg = user32.GetForegroundWindow()+            if fg and pids:+                wpid = ctypes.wintypes.DWORD(0)+                user32.GetWindowThreadProcessId(fg, ctypes.byref(wpid))+                if wpid.value in pids:+                    # minimize the focus-stealing Fusion window, no activation+                    user32.ShowWindowAsync(fg, _SW_SHOWMINNOACTIVE)+        except Exception:+            pass+        time.sleep(0.2)+++def _start_focus_guard(seconds: float = 120.0):+    """Fire-and-forget the focus guard on a daemon thread. Never raises."""+    try:+        threading.Thread(target=_focus_guard, args=(seconds,), daemon=True).start()+    except Exception:+        pass++ def ensure_fusion_running(fusion_info: dict, wait_addin: bool = False) -> dict | None:     """Launch Fusion 360 if not already running. @@ -312,17 +402,14 @@ def ensure_fusion_running(fusion_info: dict, wait_addin: bool = False) -> dict |         return {"success": False, "error": "Fusion 360 executable not found"}      try:-        # Explicit DEVNULL on all three std streams + CREATE_NO_WINDOW: the bridge-        # is itself a detached, console-less child of AD, so inherited handles are-        # invalid. Passing them through to the Fusion launcher risks a hang.-        _cnw = subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0-        subprocess.Popen(-            [exe_path],-            stdin=subprocess.DEVNULL,-            stdout=subprocess.DEVNULL,-            stderr=subprocess.DEVNULL,-            creationflags=_cnw,-        )+        # Launch MINIMIZED + NOT ACTIVATED so Fusion never yanks focus off the+        # user's work (see the background-launch block above). Explicit DEVNULL on+        # all three std streams + CREATE_NO_WINDOW: the bridge is itself a detached,+        # console-less child of AD, so inherited handles are invalid.+        _popen_fusion_background(exe_path)+        # Guard the noisy startup window: Fusion re-foregrounds itself several times+        # while its splash/main window come up, so STARTUPINFO alone isn't enough.+        _start_focus_guard(seconds=120.0)     except Exception as e:         return {"success": False, "error": f"Failed to launch Fusion 360: {e}"} 
skills/fusion-bridge-dev/SKILL.md+29
@@ -5,6 +5,35 @@ description: How to develop, version, and PUBLISH the Adom Desktop Fusion 360 br  # Fusion bridge — dev + publish workflow +## ⛔ Background-launch invariant: NEVER steal the user's foreground (HARD RULE)++The bridge runs on a machine the **user is actively working on**. Anything this bridge launches or+opens must appear in the **BACKGROUND** and must **never yank focus** off what the user is doing+(John, 2026-07-06: "make sure new windows ALWAYS only open in the background so as not to disturb my+main work"). This is not best-effort etiquette — it is enforced **in code**, and any new window-+opening path you add MUST follow it:++- **Launch minimized + not-activated.** Start GUI processes with `STARTUPINFO.wShowWindow =+  SW_SHOWMINNOACTIVE (7)` + `CREATE_NO_WINDOW`. See `_popen_fusion_background()` in `fusion_detect.py`+  — copy that, never a bare `subprocess.Popen([exe])` (which opens Fusion in the foreground).+- **Guard the startup window.** `STARTUPINFO` alone is not enough — Fusion re-foregrounds itself+  several times as its splash/main window initialize. `ensure_fusion_running()` fires+  `_start_focus_guard()`: a daemon thread that, for ~120s, minimizes (no-activate) **only** a Fusion+  window that is **currently the foreground** (it detects focus-theft via `GetForegroundWindow` ++  PID match, so it never fights a user who deliberately opened Fusion). Reuse it for any new launch.+- **The add-in still loads while minimized** — background operation (open/model/export) needs no+  visible window. So minimized costs you nothing.+- **Foreground is opt-in, only to SHOW the user something.** The ONLY time you may foreground/restore+  a Fusion window is when a verb's explicit job is to show the user (and even then, per the user's+  standing rule, you surface the exact window + their viewer). Never `SetForegroundWindow` as a side+  effect of automation.+- **Applies to every window surface**, not just the Fusion launcher: any child process, helper, or+  dialog you spawn opens background/no-activate by the same means. When you add one, add a focus-guard+  or launch flag — do not assume it will behave.++If you catch yourself about to `Popen` a GUI or call a foreground API, stop and route it through the+background-launch helpers. A window that steals the user's focus is a **regression**, full stop.+ ## You OWN this bridge (do NOT file your own bugs as adom-desktop requests) AD core only HOSTS/LOADS this bridge: the `adom-desktop` CLI, the relay/passthrough, bridge lifecycle + streaming this bridge into `…\bridges-cache\fusion360`, and the core `desktop_*` verbs. **Everything