← Commit history

v1.9.12: background-first doctrine + foreground:true opt-in everywhere

John Lauer ·287cb98205 ·1mo ago ·parent a5eea6b
6 files changed +53−12
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.11\ No newline at end of file+1.9.12\ 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.11",-    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.11/adom-bridge-fusion-v1.9.11.zip",-    "sha256": "7b3896d37966565029b928caff13189f43fa57037c3adc8612e73990bb861690",-    "size": 358860,+    "version": "1.9.12",+    "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.12/adom-bridge-fusion-v1.9.12.zip",+    "sha256": "0980efdacd06ebc1ebf6ceec5cfbfa06eae7ac8498d49dec7588c7fcb34e7cd4",+    "size": 358995,     "verbPrefixes": [         "fusion_"     ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.11",+  "version": "1.9.12",   "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.",
describe.py+3−3
@@ -197,9 +197,9 @@ _T = [     ("fusion_export_skp", "Export to SketchUp (.skp).", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/part.skp"}),      # ── Library SHOW verbs + bare-artifact wrapping (issue #29, shipped 1.9.9) ──-    ("fusion_show_symbol", "Open a schematic SYMBOL in Fusion's library editor - first-class, no hand-built EDIT strings. Pass name + libraryPath (.lbr, opened first; omit libraryPath if a library is already open), OR pass a BARE <symbol> XML fragment as symbolXml and the bridge generates a minimal wrapper library around it (deviceset + gate synthesized) so it shows in the Library Manager. Background-safe: never steals the foreground.", {"name": "str (deviceset/symbol name; optional if symbolXml carries name=)", "libraryPath": "optional str (WINDOWS path .lbr)", "symbolXml": "optional str (bare <symbol ...>...</symbol>)"}, {"success": "bool", "view": "str", "screenshot": "str path", "wrappedLibrary": "str (when wrapped)"}, 180, None, True, {"name": "ADS8588SIPM", "libraryPath": "C:/Temp/lib.lbr"}),-    ("fusion_show_footprint", "Open a FOOTPRINT (package) in Fusion's library editor. Same contract as fusion_show_symbol: name + libraryPath, or a bare <package> fragment as packageXml (wrapper library generated).", {"name": "str", "libraryPath": "optional str", "packageXml": "optional str (bare <package ...>...</package>)"}, {"success": "bool", "view": "str", "screenshot": "str path", "wrappedLibrary": "str (when wrapped)"}, 180, None, True, {"name": "ADS8588SIPM", "libraryPath": "C:/Temp/lib.lbr"}),-    ("fusion_show_3d_package", "Open the 3D CHIP view (Package3DEnvironment) for a library package - and the RELIABLE way to get a 3D model into a library: pass stepPath and it is attached AFTER the library opens (embedded <packages3d> in hand-built .lbr files is the Fusion quirk that fails; attach-after-open works). stepPath with no library/packageXml auto-generates a minimal 2-pad carrier footprint. Without stepPath it just enters the 3D view of the named package.", {"name": "str", "libraryPath": "optional str (.lbr)", "packageXml": "optional str (bare <package> fragment)", "stepPath": "optional str (WINDOWS path .step to attach)"}, {"success": "bool", "view": "str", "screenshot": "str path", "wrappedLibrary": "str"}, 300, None, True, {"name": "MYCHIP", "stepPath": "C:/Temp/chip.step"}),+    ("fusion_show_symbol", "Open a schematic SYMBOL in Fusion's library editor - first-class, no hand-built EDIT strings. Pass name + libraryPath (.lbr, opened first; omit libraryPath if a library is already open), OR pass a BARE <symbol> XML fragment as symbolXml and the bridge generates a minimal wrapper library around it (deviceset + gate synthesized) so it shows in the Library Manager. Background-safe: never steals the foreground.", {"name": "str (deviceset/symbol name; optional if symbolXml carries name=)", "libraryPath": "optional str (WINDOWS path .lbr)", "symbolXml": "optional str (bare <symbol ...>...</symbol>)", "foreground": "optional bool - user's choice to WATCH; default background"}, {"success": "bool", "view": "str", "screenshot": "str path", "wrappedLibrary": "str (when wrapped)"}, 180, None, True, {"name": "ADS8588SIPM", "libraryPath": "C:/Temp/lib.lbr"}),+    ("fusion_show_footprint", "Open a FOOTPRINT (package) in Fusion's library editor. Same contract as fusion_show_symbol: name + libraryPath, or a bare <package> fragment as packageXml (wrapper library generated).", {"name": "str", "libraryPath": "optional str", "packageXml": "optional str (bare <package ...>...</package>)", "foreground": "optional bool - user's choice to WATCH; default background"}, {"success": "bool", "view": "str", "screenshot": "str path", "wrappedLibrary": "str (when wrapped)"}, 180, None, True, {"name": "ADS8588SIPM", "libraryPath": "C:/Temp/lib.lbr"}),+    ("fusion_show_3d_package", "Open the 3D CHIP view (Package3DEnvironment) for a library package - and the RELIABLE way to get a 3D model into a library: pass stepPath and it is attached AFTER the library opens (embedded <packages3d> in hand-built .lbr files is the Fusion quirk that fails; attach-after-open works). stepPath with no library/packageXml auto-generates a minimal 2-pad carrier footprint. Without stepPath it just enters the 3D view of the named package.", {"name": "str", "libraryPath": "optional str (.lbr)", "packageXml": "optional str (bare <package> fragment)", "stepPath": "optional str (WINDOWS path .step to attach)", "foreground": "optional bool - user's choice to WATCH; default background"}, {"success": "bool", "view": "str", "screenshot": "str path", "wrappedLibrary": "str"}, 300, None, True, {"name": "MYCHIP", "stepPath": "C:/Temp/chip.step"}),     ("fusion_demo_progress", "Poll a running fusion_demo tour: {percent, elapsedSec, estimatedSec, step, stepLabel, done, failed, shots}. Poll about once a second and render a REAL progress bar. When done:true the shots array carries base64 images (pass imagesAs:paths to skip the encoding).", {"demoId": "required str (from fusion_demo mode:tour; omitted = the single running tour)", "imagesAs": "optional str paths|base64|both"}, {"percent": "int", "step": "str", "stepLabel": "str", "done": "bool", "shots": "list"}, 15, None, False, {"demoId": "tour-abc123"}),      ("fusion_export_iges", "Export to IGES.", {"outputPath": "required str"}, {"format": "str"}, 300, None, True, {"outputPath": "C:/out/board.iges"}),
dev-skills/fusion-background-etiquette/SKILL.md+35
@@ -5,6 +5,17 @@ description: The invariant every code change must preserve - Fusion windows NEVE  # fusion-background-etiquette - Fusion never steals the foreground +**The doctrine (John, 2026-08-15, verbatim):**++> all of your verbs should do their work in the background if they can, meaning its rude+> to force foreground a fusion window cuz a user using your bridge is doing other ai+> thread prompts and typing while you do your work. so make sure all verbs try to work in+> the background when they open windows. i know sometimes you have to foreground (fg) to+> do ui stuff that can only be done in the fg, but its rude. so you have to try super+> hard to figure out how to do it in the background, and then in the worst cases ONLY+> then do it in the foreground. but of course always let the user choose to fg a window+> if they want to cuz they may want to look at what you're doing.+ **The invariant (John, 2026-08-14):** a user is typing in their AI threads while this bridge drives Fusion. Any window this bridge causes to appear - the app itself, a document, a picker, a recovery dialog - must arrive in the BACKGROUND. Screenshots,@@ -12,6 +23,30 @@ exports, view switches, and builds all work on background windows (PrintWindow r occluded windows fine), so there is no functional excuse for a focus steal. A stolen foreground mid-keystroke is the single most user-hostile thing this bridge can do. +## The escalation ladder (exhaust each rung before the next)++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.+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).+4. **Foreground, the documented last resort** - ONLY when input genuinely requires+   focus (real keystrokes into a focused field that UIA cannot set). Every such verb:+   (a) is a conscious, documented exception here, (b) announces itself with an+   on-screen caption first, (c) restores the user's foreground after.++**The user can ALWAYS choose foreground:** every window-raising verb accepts+`foreground: true` - the dispatcher then skips the envelope entirely so the user can+watch. Their choice is the only non-rude foreground.++**Standing audit item:** the legacy deliberate-foreground exceptions (prefs_open,+mcp_enable, demo sign-in leg) predate rung 2 - re-audit each against background UIA+before trusting its exception status.+ ## The mechanism: the background envelope  `fusion_detect.begin_background_envelope(seconds, minimize=False)` - refcounted, so
server.py+9−3
@@ -2599,7 +2599,8 @@ def _orchestrate_attach_3d_package(args: dict) -> dict:     # The attach flow raises documents AND modal dialogs over several minutes - arm its own     # long envelope so the Save dialog at the end is still covered (2026-08-15: the dispatch     # envelope had expired by dialog time and the dialog kept the user's foreground).-    begin_background_envelope(600.0)+    if not args.get("foreground"):+        begin_background_envelope(600.0)     open_res = _orchestrate_open_lbr({"filePath": file_path})     if not open_res.get("success"):         return {"success": False, "error": "open_lbr failed: " + str(open_res.get("error")), "data": {"steps": steps}}@@ -6259,8 +6260,13 @@ def dispatch_command(command: str, args: dict, caller_identity: dict = None) ->         "show_symbol", "show_footprint", "show_3d_package",     }     if command in _WINDOW_RAISING:-        _sec = ADDIN_COMMAND_TIMEOUTS.get(command, 60) + 20-        begin_background_envelope(float(_sec), minimize=(command == "start"))+        if (args or {}).get("foreground"):+            # The USER's choice, always honored (John, 2026-08-15): they may want to WATCH.+            # No envelope - the window may come to front and stay there.+            args = {k: v for k, v in args.items() if k != "foreground"}+        else:+            _sec = ADDIN_COMMAND_TIMEOUTS.get(command, 60) + 20+            begin_background_envelope(float(_sec), minimize=(command == "start"))      # Direct handlers (don't need the add-in)     handler = COMMAND_HANDLERS.get(command)