← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.342\ No newline at end of file+1.9.343\ No newline at end of file
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.342",+  "version": "1.9.343",   "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.",
dashboard/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "fusion-dashboard"-version = "1.9.342"+version = "1.9.343" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.342"+version = "1.9.343" edition = "2021"  [dependencies]
dashboard/src/main.rs+10−34
@@ -283,40 +283,16 @@ fn action(ai_thread: &str, target: &str, name: &str, args: &serde_json::Value) -         }         "signin" => bridge(ai_thread, target, "fusion_signin", &a),         "tour" => {-            let r = bridge(ai_thread, target, "fusion_demo", "{\"mode\":\"tour\"}");-            // FLOATY TOUR REMOTE (John, 2026-08-24: "a play/pause/next/prev/exit window or-            // dialog or floaty giving me an easy way to stop the demo"): a compact pup-            // window on the user's desktop loading this dashboard's own /?remote=<demoId>-            // view - transport buttons + the live caption, clickable while they watch-            // Fusion. Best-effort: no pup, no problem, the dashboard page has the same-            // controls. Opened FOREGROUND deliberately - the user just pressed Run.-            let dug = {-                let d = unwrap_data(&r);-                if d["demoId"].is_string() { d }-                else if let Some(o) = d["output"].as_str() {-                    serde_json::from_str::<serde_json::Value>(o).unwrap_or(d)-                } else { d }-            };-            if let Some(id) = dug["demoId"].as_str() {-                let url = format!("{}?remote={}", url_for(current_port()), id);-                let args = serde_json::json!({"url": url, "sessionId": format!("fusion-tour-remote-{}", id),-                    "width": 320, "height": 420, "left": 40, "top": 120,-                    "foreground": true, "userRequestedForeground": true,-                    "foregroundReason": "the user just started the Fusion tour; this is its stop/pause remote"}).to_string();-                let ai2 = ai_thread.to_string(); let t2 = target.to_string();-                std::thread::spawn(move || {-                    match Command::new("adom-bridge")-                        .args(["--ai-thread", &ai2, "--target", &t2, "pup_open_window", &args])-                        .output() {-                        Ok(o) if o.status.success() =>-                            eprintln!("[dash] tour remote floaty opened"),-                        Ok(o) => eprintln!("[dash] tour remote floaty FAILED: {}",-                            String::from_utf8_lossy(&o.stderr).chars().take(200).collect::<String>()),-                        Err(e) => eprintln!("[dash] tour remote floaty spawn error: {}", e),-                    }-                });-            }-            r+            // THE REMOTE IS AB'S GLASS PANEL NOW (John, 2026-09-01: "ab's new demo control+            // panel that has play/pause/next/prev and is a floaty glass like interface").+            // remote:true makes the tour show desktop_demo_panel itself and drain its+            // presses onto the same transport flags this page's buttons set, so both work+            // mid-tour. The earlier pup floaty (a 320x420 window of this page's ?remote view)+            // is retired: two remotes on screen confused the story, and the glass panel+            // never steals focus. preset:electronics is the eight-stop showcase: library in,+            // symbol, footprint, 3D chip, ganged view, schematic, 2D board, 3D board.+            bridge(ai_thread, target, "fusion_demo",+                   "{\"mode\":\"tour\",\"preset\":\"electronics\",\"remote\":true}")         }         "tour_control" => bridge(ai_thread, target, "fusion_demo_control", &a),         "tour_progress" => bridge(ai_thread, target, "fusion_demo_progress", &a),
describe.py+1−1
@@ -74,7 +74,7 @@ _T = [      {"submitted": "bool", "steps": "list", "narrate": "str"},      90, "fusion_readiness", True, {"code": "123456"}),     ("fusion_demo", "FIRST-TIME-USER DEMO in one verb - the thing to run when someone new wants to SEE what this bridge does (HD's installer calls this). Takes them from nothing to a real board: launches Fusion, FINISHES the Autodesk sign-in - it OPENS the sign-in in the user's NATIVE browser IN THE BACKGROUND (never interrupting their work) and returns signinWhere/signinProfile/signinSession so you can tell them exactly where it is waiting, then offer three paths: they finish it, you foreground it, or (with their OK) you drive it. Native browser via ABE/nbrowser_* because that profile is already signed into Autodesk - never pup, which is anonymous, sets up APS cloud search (or explains it if declined) and runs a LIVE sample search so they see 2-second results, then opens an electronics PROJECT and walks SCHEMATIC -> 2D BOARD -> 3D BOARD, screenshotting each view to show them. STAGED + RESUMABLE: every call advances as far as it safely can and returns stage/done/narrate/screenshots plus a _hint with the exact next action; keep calling it until done:true. NEVER leave a first-time user parked on the sign-in screen - that is the failure this verb exists to prevent. See the 'fusion-demo' skill. TOUR MODE (issue #29): pass {mode:'tour'} for the ASYNC six-content-type showcase - it stages the bundled sample library and walks symbol -> footprint -> device -> 3D package (plus schematic/2D/3D board when a query is given), returns {demoId, estimatedSec, steps} immediately, reports structured progress via fusion_demo_progress for a real progress bar, runs in the BACKGROUND by default (foreground:true only when the user wants to watch), and its final progress poll carries the screenshot array.",-     {"query": "optional str (design to demo; else picked from APS search)", "stage": "optional str ('skip_aps' to proceed without cloud search)"},+     {"query": "optional str (design to demo; else picked from APS search)", "stage": "optional str ('skip_aps' to proceed without cloud search)", "mode": "optional str ('tour' = the async showcase; poll fusion_demo_progress {demoId})", "preset": "optional str ('electronics' = library import, symbol, footprint, 3D chip, ganged component view, schematic, 2D board, 3D board - the eight-stop showcase; omit for the full tour incl. Data Hub, APS, MCP, exports)", "remote": "optional bool (tour mode: show ab's floaty glass PLAY/PAUSE/NEXT/BACK/STOP panel on the desktop; its presses drive the same transport flags as fusion_demo_control, so the dashboard buttons and the remote can be mixed)", "captions": "optional bool (default true: one desktop caption per stop)", "foreground": "optional bool (default false: background envelope, never steals focus)"},      {"stage": "str", "done": "bool", "narrate": "str", "screenshots": "[{label,path}]", "steps": "[str]", "apsSampleSearch": "[{name,project}]"},      240, "fusion_demo", True, {}), 
install.sh+1−1
@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.9.342"+DASH_VERSION="1.9.343" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.342",+  "version": "1.9.343",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {
page.json+11−27
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.342",+  "version": "1.9.343",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {@@ -11,6 +11,10 @@     "uninstall": "./uninstall.sh"   },   "tags": [+    "ab",+    "bridge",+    "fusion",+    "desktop-app",     "fusion360",     "autodesk",     "cad",@@ -75,7 +79,10 @@     "build a basic parts library",     "add a 3d model to a component",     "make a fusion 360 library with 3d",-    "set up the fusion bridge"+    "set up the fusion bridge",+    "fusion dashboard",+    "fusion status page",+    "open the fusion launch page"   ],   "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).",   "brief": "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.",@@ -104,28 +111,5 @@       "label": "Part volume",       "prompt": "How much material does each printed part in this assembly use?"     }-  ],-  "agent_permissions": {-    "allow_bash": [],-    "allow": [-      "adom-desktop-fusion-bridge read-only verbs (via the adom-bridge CLI): fusion_readiness, fusion_get_app_state, fusion_addin_status, fusion_check_dialogs - state queries; they change nothing.",-      "adom-desktop-fusion-bridge read-only verbs: fusion_alerts (reads Fusion's own log for toasts), fusion_document_info, fusion_window_info, fusion_board_info, fusion_library_parts - inspect only.",-      "adom-desktop-fusion-bridge read-only verbs: fusion_prove_result (a persisted run report), fusion_plan_library_import (report-only preflight), fusion_list_cloud_projects, fusion_describe.",-      "adom-desktop-fusion-bridge evidence captures fusion_screenshot_fusion and fusion_capture_library_views read pixels and write local image files only."-    ],-    "soft_deny": [-      "adom-desktop-fusion-bridge lifecycle verbs fusion_stop, fusion_kill, fusion_restart_for_addin close or kill the Fusion application - keep the human prompt.",-      "adom-desktop-fusion-bridge fusion_install_fusion silently installs Autodesk Fusion on the user's machine.",-      "adom-desktop-fusion-bridge fusion_cleanup_cloud_files deletes files from the user's Autodesk cloud hub.",-      "adom-desktop-fusion-bridge managed-library verbs (fusion_managed_library_push/pull/create/link/unlink) publish to or overwrite the user's library.io libraries."-    ],-    "environment": [-      "adom-desktop-fusion-bridge is the Fusion 360 bridge for Adom Bridge; its fusion_* verbs are invoked through the adom-bridge CLI and drive Autodesk Fusion on the user's own desktop.",-      "adom-desktop-fusion-bridge desktop actions also pass Adom Bridge's own human-approval gate (caller identity, per-call reason, Activity Log) - a container approval is the first of two gates."-    ]-  },-  "author": {-    "name": "John Lauer",-    "email": "[email protected]"-  }-}\ No newline at end of file+  ]+}
server.py+84−1
@@ -9602,6 +9602,72 @@ _TOUR_EXPLAIN = { }  +# ── The on-screen remote (John, 2026-09-01): ab's floaty glass PLAY/PAUSE/NEXT panel ──+# desktop_demo_panel is a shared transport: its buttons only QUEUE presses for the driver+# (us) to poll, they never run anything, so it adds no permission surface. We map presses+# onto the tour's existing transport flags (paused / skipNext / replayPrev / abort), the+# same ones fusion_demo_control sets, so the dashboard's buttons and the desktop remote+# drive one mechanism and can be used interchangeably mid-tour.+_TOUR_PANEL_ID = "fusion-tour"+++def _tour_panel_show(state, stops, stage, playing=True, reason=""):+    """Create/update the remote (idempotent by id). Best-effort, never raises."""+    try:+        _ad_call("desktop_demo_panel", {+            "action": "show", "id": _TOUR_PANEL_ID, "title": "Fusion bridge tour",+            "stops": stops, "stage": int(stage), "state": "playing" if playing else "paused",+            "position": "bottom-right",+            "reason": reason or "Guided Fusion tour: this remote pauses, skips, or replays the tour steps"},+            timeout=10)+        state["remote"] = {"shown": True, "stage": int(stage)}+    except Exception:+        pass+++def _tour_panel_pump(state):+    """Drain the remote's presses into the tour's transport flags. Returns the list of+    button names seen (for the progress stream). Never raises."""+    seen = []+    try:+        r = _ad_call("desktop_demo_panel", {"action": "poll", "id": _TOUR_PANEL_ID}, timeout=8) or {}+        events = _find_key(r, "events") or []+        for ev in events:+            b = str((ev or {}).get("button") or "").lower()+            seen.append(b)+            if b == "pause":+                state["paused"] = True+            elif b == "play":+                state["paused"] = False+            elif b == "next":+                state["skipNext"] = True; state["paused"] = False+            elif b == "back":+                state["replayPrev"] = True; state["paused"] = False+            elif b == "stop":+                state["abort"] = True; state["paused"] = False+        if seen:+            state.setdefault("remotePresses", []).extend(seen[-8:])+    except Exception:+        pass+    return seen+++def _tour_panel_hide():+    try:+        _ad_call("desktop_demo_panel", {"action": "hide", "id": _TOUR_PANEL_ID}, timeout=8)+    except Exception:+        pass+++# The showcase John asked for (2026-09-01): library in, then symbol, footprint, 3D chip,+# the ganged component view, a schematic with that part, the 2D board, the 3D board.+# Drops the platform stops (datahub / aps / mcp / exports) which belong to the full tour.+_TOUR_PRESETS = {+    "electronics": ["launch", "stage", "library", "symbol", "footprint", "3d-package", "device",+                    "make-project", "open-design", "schematic", "2d-board", "3d-board"],+}++ def _tour_caption(text: str, seconds: int = 45):     """One desktop caption per tour step (id-replace: each step's caption replaces the     last, so the screen always narrates the CURRENT step). Best-effort, never raises."""@@ -9662,6 +9728,11 @@ def _orchestrate_demo_tour(args: dict) -> dict:             ("3d-board", "3D board", _est("3d-board", 20)),             ("exports", "Exporting: STEP + STL (of a dozen formats)", _est("exports", 25)),         ]+    preset = (args.get("preset") or "").lower()+    if preset in _TOUR_PRESETS:+        keep = _TOUR_PRESETS[preset]+        steps = sorted([s for s in steps if s[0] in keep], key=lambda s: keep.index(s[0]))+    remote = bool(args.get("remote"))     est = sum(s[2] for s in steps)     state = {"demoId": demo_id, "percent": 0, "elapsedSec": 0, "estimatedSec": est,              "step": steps[0][0], "stepLabel": steps[0][1], "done": False, "failed": False,@@ -9702,6 +9773,9 @@ def _orchestrate_demo_tour(args: dict) -> dict:             begin_background_envelope(float(est + 60))         done_est = 0         _docs_before_tour = _open_doc_names()   # tour hygiene: close only what WE open+        _stop_labels = [s[1] for s in steps]+        if remote:+            _tour_panel_show(state, _stop_labels, 0, playing=True)         try:             _VIEW_STEPS = {"symbol", "footprint", "device", "schematic", "2d-board", "3d-board", "library"}             _last_view_step = None@@ -9709,11 +9783,18 @@ def _orchestrate_demo_tour(args: dict) -> dict:             while _step_i < len(steps):                 key, label, dur = steps[_step_i]                 _step_i += 1-                # ── TRANSPORT (fusion_demo_control): boundary checks, never mid-verb ──+                # ── TRANSPORT (fusion_demo_control + the on-screen remote): boundary+                # checks, never mid-verb. The remote's presses are drained here and while+                # paused, onto the same flags the dashboard's buttons set. ──+                if remote:+                    _tour_panel_pump(state)+                    _tour_panel_show(state, _stop_labels, _step_i - 1, playing=not state.get("paused"))                 if state.pop("abort", None):                     raise RuntimeError("tour exited by the user (transport control)")                 while state.get("paused") and not state.get("abort"):                     state.update(stepLabel="Paused - press play to continue", stepCaption="Paused.")+                    if remote:+                        _tour_panel_pump(state)                     time.sleep(1.5)                 if state.pop("abort", None):                     raise RuntimeError("tour exited by the user (transport control)")@@ -10009,6 +10090,8 @@ def _orchestrate_demo_tour(args: dict) -> dict:                     _ad_call("desktop_caption", {"action": "hide", "id": "fusion-tour"}, timeout=8)                 except Exception:                     pass+            if remote:+                _tour_panel_hide()             _demo_tour_persist(state)      threading.Thread(target=_run, daemon=True, name="fusion-demo-tour").start()