← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.243\ No newline at end of file+1.9.244\ 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.243",-  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.243/adom-bridge-fusion-v1.9.243.zip",-  "sha256": "197960ef701c192fd57b93f366bf4466b47fa539a7586dc243533063139fd5de",-  "size": 540244,+  "version": "1.9.244",+  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.244/adom-bridge-fusion-v1.9.244.zip",+  "sha256": "4e6d8a9e878a875bf1c8d624662a84322f1fb59506d79d97c007eb0b4794afb1",+  "size": 540459,   "verbPrefixes": [     "fusion_"   ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.243",+  "version": "1.9.244",   "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.243"+version = "1.9.244" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.243"+version = "1.9.244" edition = "2021"  [dependencies]
describe.py+3−3
@@ -276,9 +276,9 @@ _T = [      {"parts": "optional [{package, lbrPath, modelPath}] - the parts you intend to import (drives the estimate)", "stepPath": "optional single .step when not passing parts[]", "libraryPath": "optional .lbr for context"},      {"success": "bool", "fusion": "{installed, running, addinReady, status, message}", "plan": "[{stage, label, estimatedSec, confidence, needsUserConsent?}]", "estimatedSec": "float", "estimatedSecIfAlreadyOpen": "float", "summary": "str"},      20, None, True, {"parts": [{"package": "AL8843SP-13", "lbrPath": "C:/tmp/al8843.lbr", "modelPath": "C:/tmp/al8843.step"}]}),-    ("fusion_live_feed", "A ~1 fps look INTO Fusion while the bridge drives it, published to a shotlog channel as tagged kinds: [main #n] = the Fusion window each tick (the video), [popup #n.m] = every owned dialog/panel over it tied to its tick (Fusion draws these as separate windows a main capture omits). Proof frames from the import chain are the third kind (kind:proof, with step labels) and are NOT produced here - this feed shows what happens BETWEEN proofs, e.g. during boot or a restart, so a human can see open tabs and dialogs instead of guessing. Runs in the background, never foregrounds. The wiki may choose not to render 1 fps; the bridge owns the feed regardless.",-     {"action": "start|stop|status", "channel": "optional str (default fusion-live)", "intervalSec": "optional float (default 1.0)", "maxSec": "optional float (default 600)"},-     {"success": "bool", "started": "bool", "channel": "str", "frames": "int"}, 15, None, True, {"action": "start", "channel": "fusion-live"}),+    ("fusion_live_feed", "A ~1 fps look INTO Fusion while the bridge drives it, kept as a BOX-SIDE ring buffer (~10 min). Each tick records the Fusion main window (kind:main - the video) and every owned popup over it (kind:popup - dialogs, Browser/tool panels, the Adom banner; Fusion draws these as separate windows a main capture omits), each a path on this machine tied to its tick. Drain from the container with {action:frames, since:<tick>}, read_file each path, and publish where a human looks - tools/live_feed_drain.py does exactly that into shotlog. Proof frames are NOT here: fusion_prove_result evidence[] carries kind:proof with step labels, so the three kinds are distinguishable at the source. Exists so a human can see open tabs and dialogs during boot or a restart instead of guessing.",+     {"action": "start|stop|status|frames", "intervalSec": "optional float (default 1.0)", "maxSec": "optional float (default 600)", "since": "frames only: return ticks newer than this"},+     {"success": "bool", "running": "bool", "tick": "int", "frames": "[{tick, kind:main|popup, path, title, at}]"}, 15, None, True, {"action": "frames", "since": 0}),     ("fusion_restart_for_addin", "Finish add-in setup by restarting Fusion, SAVING the user's open documents first. Call this when any verb returns errorCode addin_restart_required (the add-in was installed/updated while Fusion was already open, and Fusion only loads add-ins at launch). ASK THE USER FIRST when a human is present - the failing verb's payload carries userMessage plus openDocuments/unsavedDocuments so you can show them what is open. Refuses with unsaved_named_document rather than discarding a never-saved document.",      {"save": "optional bool (default true) - save open documents before restarting"},      {"success": "bool", "addinReady": "bool", "tookSec": "float", "steps": "[{step, ok}]"},
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.243"+DASH_VERSION="1.9.244" 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.243",+  "version": "1.9.244",   "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−8
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.243",+  "version": "1.9.244",   "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,9 +111,5 @@       "label": "Part volume",       "prompt": "How much material does each printed part in this assembly use?"     }-  ],-  "author": {-    "name": "John Lauer",-    "email": "[email protected]"-  }-}\ No newline at end of file+  ]+}
server.py+66−58
@@ -13529,89 +13529,97 @@ def _human_secs(v: float) -> str:     return "%dm %02ds" % (m, sec) if sec else "%dm" % m  -_LIVE_FEED = {"thread": None, "stop": False, "frames": 0, "channel": None}+_LIVE_FEED = {"thread": None, "stop": False, "tick": 0, "frames": [], "startedAt": None}+_LIVE_FEED_MAX = 600      # ring buffer: ~10 min at 1 fps   def _handle_live_feed(fusion_info: dict, args: dict) -> dict:-    """A ~1 fps look INTO Fusion while the bridge drives it, published as three KINDS.--    John, 2026-08-28, after a chain restarted Fusion under him: "had you shown me a-    screenshot of fusion's interface i could have seen if there were any open tabs or not-    ... i literally should be getting screenshots like every 5 seconds during boot up ...-    or maybe each second, so like a 1 fps video." And: the frames need telling apart --    a Browser-panel popup, the main window, and a proof shot are three different things-    and a viewer must never have to guess which it is looking at.--    kind=main   the Fusion main window, the 1 fps video. One card per tick.-    kind=popup  every OWNED popup over it (dialogs, Browser/tool panels, the Adom banner),-                each tagged with the tick it belongs to. Fusion draws these as separate-                windows that a main-frame capture omits, so they ride alongside, never-                instead of, the main frame.-    kind=proof  is NOT produced here. Proof frames are the evidence the import chain-                captures (symbol / footprint / 3d / ganged) and they carry their own labels;-                the feed exists so the human can see what happened BETWEEN proofs.--    The wiki may choose not to render 1 fps; that is fine. The bridge owns the feed and-    publishes it to shotlog under `channel`, so a human always has somewhere to look.-    args: {action: start|stop|status, channel?, intervalSec? (default 1.0), maxSec? (600)}+    """A ~1 fps look INTO Fusion while the bridge drives it, as a box-side ring buffer.++    John, 2026-08-28: "i literally should be getting screenshots like every 5 seconds+    during boot up ... or maybe each second, so like a 1 fps video" - after a chain+    restarted Fusion under him and nothing ever showed him what was in it. And: a Browser+    panel popup, the main window, and a proof shot must never be confused.++    DESIGN, and why it is shaped this way. The first version (1.9.233) ran here on the+    Windows bridge and shelled out to `adom-shotlog` while reading `localSafePath` - a+    container tool and a container-side key. It reported running:true, frames:0 forever.+    The bridge can only ever produce BOX paths, so that is what it produces: each tick+    records the main-window capture and every owned popup (dialogs, Browser/tool panels,+    the Adom banner - Fusion draws these as separate windows a main capture omits) into a+    ring buffer, tagged. Something on the container side drains it with+    {action:"frames", since:<tick>}, read_file's the paths, and publishes them wherever a+    human is looking (shotlog, or the wiki if it chooses to render 1 fps). The bridge owns+    the feed; the presenter is pluggable.++    kind=main   the Fusion main window, one per tick (the video)+    kind=popup  each owned popup over it, tied to its tick+    kind=proof  NOT produced here: the import chain's evidence frames carry kind:"proof"+                and a step label, so the three are distinguishable at the source.++    args: {action: start|stop|status|frames, intervalSec? (default 1.0), maxSec? (600),+           since? (tick; frames newer than this)}     """-    import threading as _th, time as _t, subprocess as _sp+    import threading as _th, time as _t     action = str(args.get("action") or "status").lower()+    alive = bool(_LIVE_FEED["thread"] and _LIVE_FEED["thread"].is_alive())     if action == "status":-        return {"success": True, "running": bool(_LIVE_FEED["thread"] and _LIVE_FEED["thread"].is_alive()),-                "frames": _LIVE_FEED["frames"], "channel": _LIVE_FEED["channel"]}+        return {"success": True, "running": alive, "tick": _LIVE_FEED["tick"],+                "buffered": len(_LIVE_FEED["frames"]), "startedAt": _LIVE_FEED["startedAt"]}+    if action == "frames":+        since = int(args.get("since") or 0)+        fr = [f for f in _LIVE_FEED["frames"] if f["tick"] > since]+        return {"success": True, "running": alive, "tick": _LIVE_FEED["tick"],+                "frames": fr[-200:], "count": len(fr),+                "_hint": ("Each frame is a BOX path (this machine); read_file it from the "+                          "container and publish it. kind is main|popup; proof frames come "+                          "from fusion_prove_result evidence[] with kind:'proof'. Pass "+                          "since:<tick> to get only what you have not seen.")}     if action == "stop":         _LIVE_FEED["stop"] = True-        return {"success": True, "stopped": True, "frames": _LIVE_FEED["frames"]}-    if _LIVE_FEED["thread"] and _LIVE_FEED["thread"].is_alive():-        return {"success": True, "alreadyRunning": True, "channel": _LIVE_FEED["channel"],-                "frames": _LIVE_FEED["frames"]}-    channel = str(args.get("channel") or "fusion-live")+        return {"success": True, "stopped": True, "tick": _LIVE_FEED["tick"]}+    if alive:+        return {"success": True, "alreadyRunning": True, "tick": _LIVE_FEED["tick"]}     interval = max(0.5, float(args.get("intervalSec") or 1.0))     max_sec = min(3600.0, float(args.get("maxSec") or 600.0))-    _LIVE_FEED.update(stop=False, frames=0, channel=channel)--    def _inject(path, desc):-        try:-            _sp.run(["adom-shotlog", "inject", "-c", channel, "-d", desc, "-s",-                     "desktop_screenshot_window", "--native", path],-                    capture_output=True, text=True, timeout=20)-        except Exception:-            pass+    _LIVE_FEED.update(stop=False, tick=0, frames=[], startedAt=_t.time())      def _run():-        t0 = _t.time(); tick = 0+        t0 = _t.time()         while not _LIVE_FEED["stop"] and _t.time() - t0 < max_sec:-            tick += 1+            _LIVE_FEED["tick"] += 1+            tick = _LIVE_FEED["tick"]             try:                 info = get_fusion_window_info() or {}                 hwnd = info.get("hwnd") or (info.get("mainWindow") or {}).get("hwnd")                 if hwnd:                     shot = _ad_call("desktop_screenshot_window", {"hwnd": int(hwnd)}, timeout=20) or {}-                    main = shot.get("localSafePath")-                    pops = shot.get("screenshots") or []+                    # BOX paths: fullPath (main) and fullPathHost / localFullPath per popup.+                    main = shot.get("fullPath") or shot.get("fullPathHost") or shot.get("path")                     title = str(shot.get("title") or info.get("title") or "Fusion")[:60]+                    now = _t.time()                     if main:-                        _inject(main, "[main #%d] %s | %d popup(s)" % (tick, title, len(pops)))-                        _LIVE_FEED["frames"] += 1-                    for j, sp_ in enumerate(pops, 1):-                        if sp_.get("localSafePath"):-                            _inject(sp_["localSafePath"],-                                    "[popup #%d.%d] %s" % (tick, j, str(sp_.get("title") or "dialog")[:50]))+                        _LIVE_FEED["frames"].append({"tick": tick, "kind": "main", "at": now,+                                                     "path": main, "title": title,+                                                     "popups": len(shot.get("screenshots") or [])})+                    for k, sp_ in enumerate(shot.get("screenshots") or [], 1):+                        pth = sp_.get("fullPathHost") or sp_.get("fullPath") or sp_.get("path")+                        if pth:+                            _LIVE_FEED["frames"].append({"tick": tick, "kind": "popup", "at": now,+                                                         "index": k, "path": pth,+                                                         "title": str(sp_.get("title") or "dialog")[:50]})+                    if len(_LIVE_FEED["frames"]) > _LIVE_FEED_MAX:+                        del _LIVE_FEED["frames"][: len(_LIVE_FEED["frames"]) - _LIVE_FEED_MAX]             except Exception:                 pass             _t.sleep(interval)         _LIVE_FEED["thread"] = None     _LIVE_FEED["thread"] = _th.Thread(target=_run, daemon=True); _LIVE_FEED["thread"].start()-    return {"success": True, "started": True, "channel": channel, "intervalSec": interval,-            "maxSec": max_sec,-            "_hint": "⚠️ KNOWN LIMIT (2026-08-28): this runs on the Windows bridge, which has no "-                     "adom-shotlog and no container paths, so frames do NOT reach shotlog from here "-                     "yet - drive the capture loop from the container until the ring-buffer redesign "-                     "lands. Intended contract: frames land in shotlog channel %r tagged [main #n] (the 1 fps video) and "-                     "[popup #n.m] (dialogs/panels over it, tied to their tick). Proof frames "-                     "from the import chain are a third kind and carry their own step labels. "-                     "Open with: adom-shotlog open -c %s --skinny. Stop with action:stop." % (channel, channel)}+    return {"success": True, "started": True, "intervalSec": interval, "maxSec": max_sec,+            "_hint": ("Capturing into a box-side ring buffer. Drain it from the container with "+                      "fusion_live_feed {action:'frames', since:<lastTick>}, read_file each "+                      "path, and publish (e.g. adom-shotlog inject -c fusion-live -d "+                      "'[main #n] ...'). tools/live_feed_drain.py does exactly that.")}   def _handle_restart_for_addin(fusion_info: dict, args: dict) -> dict:
skills/fusion-ralph-test-via-wiki/SKILL.md+3−2
@@ -29,7 +29,8 @@ different tests. Only the second proves anything to a user. This is the second. ## The loop  ```-1. fusion_live_feed {action:start, channel:fusion-live}     <- SEE Fusion before touching it+1. python3 tools/live_feed_drain.py --target <Box> --channel fusion-live &   <- SEE Fusion first+   (starts fusion_live_feed on the box and drains its ring buffer into shotlog, tagged) 2. pup_open_window   {sessionId, url: wiki.adom.inc/<owner>/<part>}   (your own window) 3. tap the page's fetch (below) so every verb it sends is recorded 4. toggle the lane to Fusion; press Connect; confirm wc-status says "Connected"@@ -98,7 +99,7 @@ restarted - and the user owns that restart if they said so.  ## Reading the three kinds of frame -`fusion_live_feed` publishes to shotlog as `[main #n]` (the window each tick - the video)+`fusion_live_feed` keeps a box-side ring buffer; `tools/live_feed_drain.py` publishes it to shotlog as `[main #n]` (the window each tick - the video) and `[popup #n.m]` (each owned dialog/panel over it, tied to its tick - Fusion draws these as separate windows a main capture omits). The chain's evidence frames are `kind:proof` with a step label. A Browser panel, the main frame, and a proof shot are three different things; a