← Commit history

v1.9.21: dialog-free make_test_project + user-yield watcher

John Lauer ·d4268fd351 ·1mo ago ·parent 1c41dae
6 files changed +88−21
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.20\ No newline at end of file+1.9.21\ 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.20",-    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.20/adom-bridge-fusion-v1.9.20.zip",-    "sha256": "ad0886d59240463f882dca229661e046b5d274052cf5e4f0d0e9f8fd5594d442",-    "size": 364613,+    "version": "1.9.21",+    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.21/adom-bridge-fusion-v1.9.21.zip",+    "sha256": "be152ecff1067ca2676be2084cccff429b2c21dfc58ebcc29af6f7449420d6ee",+    "size": 365481,     "verbPrefixes": [         "fusion_"     ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.20",+  "version": "1.9.21",   "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+15−2
@@ -75,8 +75,21 @@ three things for its duration:      z-order WITHOUT minimizing - a minimized window renders stale in PrintWindow, and      background captures must keep working mid-operation. -The watcher is surgical: it acts only when the CURRENT foreground window belongs to the-Fusion family. A user who deliberately clicks into Fusion is never fought.+The watcher is surgical two ways: it acts only when the CURRENT foreground window+belongs to the Fusion family, AND it stands down whenever real keyboard/mouse input+happened in the last 2 seconds (GetLastInputInfo) - because a foreground change right+after user input IS the user. This second rule was added 2026-08-15 after the watcher+demoted John's own clicks and he could not raise Fusion at all; "never fights a+deliberate user" was a false claim until the input guard made it true.++**Native modal dialogs are a hard NO from automation.** ImportSCHAndBRDCmd's Open+dialogs wedged the user's Fusion (queued dialogs, disabled main window, uncancellable+by the user because the watcher buried them). Any flow that would pop a native dialog+must use the dialog-free primitive instead (Document.newDesignFromLocal covers .lbr,+.brd with placed elements, and .sch), or be a captioned, deliberate-foreground manual+verb. Recovery ritual if one ever wedges again: desktop_screenshot_window {hwnd} to see+the popups + their hwnds, then desktop_ui_click {name:"Cancel", scope:"popups"} per+queued dialog - all background.  ## Where it is armed 
fusion_detect.py+17−1
@@ -772,6 +772,20 @@ def _envelope_watch(seconds: float, minimize: bool):         user32.SetWindowPos.restype = ctypes.wintypes.BOOL         HWND_BOTTOM = HWND(1)         SWP_FLAGS = 0x0001 | 0x0002 | 0x0010  # NOSIZE | NOMOVE | NOACTIVATE+        # NEVER fight the user (John, 2026-08-15: the watcher demoted HIS clicks and he+        # could not bring Fusion up at all): if real keyboard/mouse input happened in the+        # last 2s, the foreground change is HIS - stand down for that beat.+        class _LII(ctypes.Structure):+            _fields_ = [("cbSize", ctypes.wintypes.UINT), ("dwTime", ctypes.wintypes.DWORD)]+        def _user_active_recently():+            try:+                lii = _LII(); lii.cbSize = ctypes.sizeof(_LII)+                if ctypes.windll.user32.GetLastInputInfo(ctypes.byref(lii)):+                    idle = ctypes.windll.kernel32.GetTickCount() - lii.dwTime+                    return idle < 2000+            except Exception:+                pass+            return False         end = time.time() + seconds         pids, last_refresh = set(), 0.0         while time.time() < end:@@ -781,7 +795,7 @@ def _envelope_watch(seconds: float, minimize: bool):                 last_refresh = now             try:                 fg = user32.GetForegroundWindow()-                if fg and pids:+                if fg and pids and not _user_active_recently():                     wpid = ctypes.wintypes.DWORD(0)                     user32.GetWindowThreadProcessId(fg, ctypes.byref(wpid))                     if wpid.value in pids:@@ -799,6 +813,8 @@ def _envelope_watch(seconds: float, minimize: bool):         grace_end = time.time() + 20         while time.time() < grace_end:             try:+                if _user_active_recently():+                    break  # the user is driving - the foreground is THEIRS now                 fg = user32.GetForegroundWindow()                 if not (fg and pids):                     break
server.py+50−12
@@ -5619,9 +5619,13 @@ def _orchestrate_demo_tour(args: dict) -> dict:         ("device", "Component view: symbol + package mapping", 8),         ("3d-package", "3D chip: the Package3D environment", 30),     ]-    if include_design and query:+    if include_design:+        # Design side is SELF-SUFFICIENT by default (John, 2026-08-15: customers have+        # only a component page, never a canonical cloud board): generate + import a+        # test project around the sample part. A query switches to the cloud-open path.+        steps += ([("open-design", "Opening the cloud electronics design", 150)] if query+                  else [("make-project", "Generating + importing a test project around the sample part", 60)])         steps += [-            ("open-design", "Opening the cloud electronics design", 150),             ("schematic", "Schematic view", 12),             ("2d-board", "2D board layout", 12),             ("3d-board", "3D board", 20),@@ -5694,6 +5698,15 @@ def _orchestrate_demo_tour(args: dict) -> dict:                     else:                         state.setdefault("warnings", []).append("3d-package step skipped: " + str(cp.get("error")))                         state.setdefault("failedSteps", []).append(key)+                elif key == "make-project":+                    r = _orchestrate_make_test_project({"libraryPath": state["sampleLbr"]})+                    if r.get("success"):+                        time.sleep(3)+                        _snap(label)+                    else:+                        state.setdefault("failedSteps", []).append(key)+                        state.setdefault("warnings", []).append("make-project: " + str(r.get("error"))[:120])+                        state["designOpenFailed"] = True                 elif key == "open-design":                     dispatch_command("open_cloud_file", {"fileName": query, "wait": False})                     deadline = time.time() + 170@@ -6018,16 +6031,41 @@ def _orchestrate_make_test_project(args: dict) -> dict:     brd_path = os.path.join(d, base + ".brd").replace("\\", "/")     open(sch_path, "w", encoding="utf-8").write(sch_text)     open(brd_path, "w", encoding="utf-8").write(brd_text)-    res = _handle_new_electronics_from_eagle({"schPath": sch_path, "brdPath": brd_path})-    out = {"success": bool(res.get("success")), "schPath": sch_path, "brdPath": brd_path,-           "deviceset": deviceset, "circuit": "R1 (330, generated 0603) in series with %s; VCC/GND stubs" % deviceset}-    if not res.get("success"):-        out["error"] = "import failed: " + str(res.get("error"))[:200]-        out["_hint"] = str(res.get("_hint") or "")[:200]-        return out-    out["_hint"] = ("Test project imported as a NEW electronics design. Now the whole chain works: "-                    "fusion_show_schematic -> fusion_show_2d_board -> fusion_show_3d_board. "-                    "Generated from NOTHING but the component's .lbr - no cloud, no sign-in.")+    # ⛔ DIALOG-FREE ONLY (John, 2026-08-15, after ImportSCHAndBRDCmd's native Open dialogs+    # wedged his Fusion): Document.newDesignFromLocal opens each file with ZERO dialogs and+    # ZERO foreground - the same primitive open_lbr/open_board use. The .brd (elements+    # placed + embedded libraries) INSTANTIATES the board; the .sch opens as its own+    # drawn schematic document. Never route this through new_electronics_from_eagle.+    import time as _t+    steps = []+    for label, fpath in (("board", brd_path), ("schematic", sch_path)):+        r = _proxy_to_addin("execute_text_command",+                            {"command": "Document.newDesignFromLocal %s" % fpath}, timeout=120)+        okd = bool(r.get("success"))+        if not okd:+            expected = fpath.rsplit("/", 1)[-1].rsplit(".", 1)[0].lower()+            for _ in range(10):+                _t.sleep(3)+                st = _proxy_to_addin("get_app_state", {}, timeout=10)+                if expected in str((st.get("data") or {}).get("activeDocument", "")).lower():+                    okd = True+                    break+        steps.append("%s open: %s" % (label, "ok" if okd else "FAILED: " + str(r.get("error"))[:80]))+        if not okd:+            return {"success": False, "schPath": sch_path, "brdPath": brd_path,+                    "error": "%s open failed: %s" % (label, str(r.get("error"))[:150]),+                    "data": {"steps": steps}}+        _t.sleep(2)+    out = {"success": True, "schPath": sch_path, "brdPath": brd_path,+           "deviceset": deviceset, "steps": steps,+           "circuit": "R1 (330, generated 0603) in series with %s; VCC/GND stubs" % deviceset,+           "_hint": ("Test project opened as TWO documents (board instantiated with placed parts, "+                     "schematic as its drawn sheet) via the dialog-free Document.newDesignFromLocal "+                     "path. The BOARD document is active: fusion_show_2d_board / fusion_show_3d_board "+                     "work now; for the schematic view activate its document (it opened last... the "+                     "SCHEMATIC document is actually active) - board legs may need the board doc "+                     "re-activated via Document.newDesignFromLocal or open_board. Generated from "+                     "NOTHING but the component's .lbr - no cloud, no sign-in.")}     return out