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.51: foreground etiquette contract
6 files changed
+95−13
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.50+1.9.51
CHANGELOG.md+11@@ -3,6 +3,17 @@ 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.51 - 2026-08-16++- **Foreground etiquette contract (John, after ab's demo stole his focus).** (1) The tour's+ single est-sized background envelope EXPIRED mid-run on slow tours (485-890s vs ~376s armed),+ so later steps foregrounded Fusion while the user typed - the envelope now RE-ARMS per step.+ (2) foreground:true now REQUIRES foregroundReason and shows it as an ab caption for 3 seconds+ BEFORE any focus change, so the user knows why and can yield (foreground_reason_required+ teaching refusal otherwise). (3) Every backgrounded window-raising verb annotates its response+ with windowEtiquette:"background" + a hint so the calling AI knows no focus was taken and never+ compensates by foregrounding. Contract recorded in dev-skills/fusion-background-etiquette.+ ## 1.9.50 - 2026-08-16 - **Third tour failure, last gap closed**: the tour's own bundled sample docs (ADS8588SIPM,
adom-bridge-fusion-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "fusion360",- "version": "1.9.50",- "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.50/adom-bridge-fusion-v1.9.50.zip",- "sha256": "3f2dfb5688c5785302b6c823bdc81f7f57fdf6d0f970fe1534cfe454e23d661e",- "size": 397998,+ "version": "1.9.51",+ "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.51/adom-bridge-fusion-v1.9.51.zip",+ "sha256": "916f14373464d461ab0e074c79849ef855f5c1bd22869d99359c829b995130f5",+ "size": 398944, "verbPrefixes": [ "fusion_" ],
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.9.50",+ "version": "1.9.51", "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+31@@ -23,6 +23,37 @@ 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 foreground CONTRACT (John, 2026-08-16, after ab's demo run stole his focus)++> you can allow the ai to foreground fusion, but you have to make it give you a reason+> and then you should show the reason for 3 seconds as an ab caption so the user knows+> why its being foregrounded and then can yeild. you should be returning hints to the+> caller, i.e. the ai, that you are opening these fusion windows in the background so+> it knows.++Implemented in dispatch (v1.9.51), and every future window-raising verb MUST keep it:++1. **`foreground:true` alone is REFUSED** (`errorCode: foreground_reason_required`).+ The caller must pass `foregroundReason:"<user-facing sentence>"`.+2. **The reason is shown as an ab caption for ~3 seconds BEFORE any focus change**+ (ad_client.notify + sleep(3)), so the user knows why and can yield.+3. **Background runs annotate the RESPONSE**: `windowEtiquette:"background"` plus a hint+ telling the calling AI no focus was taken, to render via fusion_state, and to never+ "helpfully" foreground afterward. The AI learns the etiquette from the verb itself.++## The envelope-expiry trap (the bug behind John's 2026-08-16 report)++The tour armed ONE envelope sized `estimate+60s` at start - but real tours ran 485-890s+(cold boots, dialog stalls), so the envelope EXPIRED mid-run and every later doc open+stole the foreground while the user typed in other threads. Rules:++- **Long orchestrations RE-ARM the envelope per step** (`begin_background_envelope(dur*3+240)`+ each loop iteration - nesting is refcounted, re-arming is free). Never size one envelope+ off an estimate that a slow cold boot can blow through.+- **Direct orchestrator calls bypass dispatch's envelope** (_orchestrate_*, _show_library_view,+ _test_project_activate called from inside another verb) - the CALLING orchestration owns+ the envelope for them.+ ## The escalation ladder (exhaust each rung before the next) 1. **API / text command** - the add-in API, EAGLE text commands, and modeling scripts
server.py+47−7@@ -6431,11 +6431,18 @@ def _orchestrate_demo_tour(args: dict) -> dict: pass def _run():- if not args.get("foreground"):+ fg = bool(args.get("foreground"))+ if not fg: begin_background_envelope(float(est + 60)) done_est = 0 try: for key, label, dur in steps:+ if not fg:+ # RE-ARM per step: one est-sized envelope EXPIRED mid-tour on real runs+ # (485-890s vs ~376s armed) and later steps stole the user's foreground+ # while they typed in other threads (John, live during ab's demo,+ # 2026-08-16). Nesting is refcounted, so re-arming is free.+ begin_background_envelope(float(dur * 3 + 240)) state.update(step=key, stepLabel=label, percent=int(100 * done_est / est), elapsedSec=int(time.time() - state["started"]))@@ -7724,19 +7731,52 @@ def dispatch_command(command: str, args: dict, caller_identity: dict = None) -> "show_symbol", "show_footprint", "show_3d_package", "make_test_project", "bind_library_3d", "show_library", "show_project", }+ _ran_backgrounded = False if command in _WINDOW_RAISING: 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"}+ # Foreground is allowed but never free (John, 2026-08-16): the CALLER must say+ # WHY, and the user gets that reason as an ab caption for ~3s BEFORE the steal,+ # so they know what is happening and can yield (or object). No reason = refuse+ # with a teaching hint - same pattern as every reason-gated Adom verb.+ fg_reason = str(args.get("foregroundReason") or "").strip()+ if not fg_reason:+ return {"success": False, "errorCode": "foreground_reason_required",+ "error": "foreground:true needs foregroundReason (a short user-facing sentence).",+ "_hint": ("Fusion verbs ALWAYS run in the background so the user is never "+ "interrupted mid-typing. If the user genuinely wants to WATCH, re-call "+ "with foreground:true AND foregroundReason:\"<why, in words the user "+ "will read>\" - it is shown as an ab caption for 3 seconds before the "+ "window comes forward, so they can yield.")}+ try:+ ad_client.notify("Fusion is coming to the foreground",+ fg_reason + " (Requested by your AI - Fusion verbs otherwise always "+ "run in the background.)", level="info", reach_user=True)+ _time.sleep(3.0) # the user's 3-second heads-up BEFORE any focus change+ except Exception:+ pass+ args = {k: v for k, v in args.items() if k not in ("foreground", "foregroundReason")} else: _sec = ADDIN_COMMAND_TIMEOUTS.get(command, 60) + 20 begin_background_envelope(float(_sec), minimize=(command == "start"))+ _ran_backgrounded = True++ def _annotate_background(res):+ # Tell the CALLING AI what just happened window-wise (John, 2026-08-16): the verb+ # ran under the background envelope - no focus was taken, nothing to apologize for,+ # and the AI should not "helpfully" foreground anything afterward.+ if _ran_backgrounded and isinstance(res, dict):+ res.setdefault("windowEtiquette", "background")+ note = ("Window opened/changed in the BACKGROUND - the user's focus was NOT touched. "+ "Render results via fusion_state. Only pass foreground:true (with "+ "foregroundReason) if the USER asked to watch.")+ h = res.get("_hint")+ res["_hint"] = (h + " " + note) if isinstance(h, str) and note not in h else (h or note)+ return res # Direct handlers (don't need the add-in) handler = COMMAND_HANDLERS.get(command) if handler is not None:- return handler(fusion_info, args)+ return _annotate_background(handler(fusion_info, args)) # Check if Fusion is installed and running before any add-in-dependent command. # We do NOT auto-launch — that causes 60s+ hangs when Fusion isn't running.@@ -7941,7 +7981,7 @@ def dispatch_command(command: str, args: dict, caller_identity: dict = None) -> pass _set_long_command(command) try:- return _proxy_to_addin(addin_cmd, args, timeout=proxy_timeout)+ return _annotate_background(_proxy_to_addin(addin_cmd, args, timeout=proxy_timeout)) finally: _clear_long_command() result = _proxy_to_addin(addin_cmd, args, timeout=proxy_timeout)@@ -7950,7 +7990,7 @@ def dispatch_command(command: str, args: dict, caller_identity: dict = None) -> # blind. Read-only verbs are excluded to avoid per-call screenshot latency. if command in MUTATING_COMMANDS: result = _merge_dialog_array(result)- return result+ return _annotate_background(result) # Unknown command — check installation/running status for helpful errors if not fusion_info.get("installed"):