← Commit history
.insiders-stage/fusion-bridge/version.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.9.368",-  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.368/adom-bridge-fusion-v1.9.368.zip",-  "sha256": "b0ab98d74bdc44fc878b77c48db3dd9f699822f34094c32b10c2bc526123721a",-  "size": 6312573,+  "version": "1.9.369",+  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.369/adom-bridge-fusion-v1.9.369.zip",+  "sha256": "1be41c798f6d70ea808b1f5ca9caea5898570c533b1707f367cefce2ffa27a6f",+  "size": 6313361,   "verbPrefixes": [     "fusion_"   ],
.release-page-head+1−1
@@ -1 +1 @@-fbca2b13a02472ce0c66c5304db57ac8231059f7\ No newline at end of file+f721a9b390ed6cd022b33eccd2f13e126f8704f2\ No newline at end of file
dashboard/ui/vendor/newbie-panel.js+179−7
@@ -112,6 +112,23 @@   var TWO_COL_MIN = 700, PHRASE_BUDGET = 336;   var MIN_API = [1, 0, 142]; +  /* Hydrogen 1.0.187+ answers a send with a JOB and reports progress on+   * GET /ai-threads/run-prompt/status?job=<uuid>. Measured on the VM: tab visible ~0.1s,+   * session id ~2.5s, sent by 3-5s, a trivial answer done ~8s. One poll a second is the+   * right cadence for that shape, and it is an in-memory read on the other end. */+  var PHASE = {+    opening:               'Opening a new AI chat…',+    prefilled:             'Prompt ready…',+    submitted:             'Sent, waiting for the AI…',+    working:               'The AI is working…',+    needs_you:             'The AI needs you. Open the tab.',+    done:                  'Done.',+    submit_failed:         'The prompt is in the tab. Press Enter there to send it.',+    submitted_unidentified:'Sent, but the AI never checked in.',+    submit_unconfirmed:    'Sent, but delivery was not confirmed.',+    still_working:         'The AI is still working. Open the tab.'+  };+   function css() {     if (document.getElementById('anp-css')) return;     var s = document.createElement('style');@@ -174,6 +191,9 @@     if (!toastHost) { toastHost = el('div', 'anp-toasts'); document.body.appendChild(toastHost); }     var t = el('div', 'anp-toast ' + (kind || 'info'), esc(msg));     toastHost.appendChild(t);+    /* retextable, so a long send can narrate its phases in ONE toast instead of+     * stacking six of them on top of each other. */+    t.setText = function (next) { t.innerHTML = esc(next); return t; };     setTimeout(function () { t.remove(); }, ms || (kind === 'err' ? 7000 : 4000));     return t;   }@@ -224,7 +244,7 @@       })       .catch(function () { return { ok: true, version: null, open: true }; });   }-  function send(base, phrase, appName, slug) {+  function send(base, phrase, appName, slug, onProgress) {     return gate(base).then(function (g) {       if (!g.ok) throw new Error('Adom needs updating to send prompts (found ' + g.version + ', needs 1.0.142). Run: adom update');       var ctl = new AbortController(), timer = setTimeout(function () { ctl.abort(); }, 120000);@@ -241,12 +261,55 @@         .then(function (res) {           clearTimeout(timer);           var j = res.j || {};-          /* THREE states, never two. delivered_to is an ARRAY; a thread that opened but did-           * not confirm delivery is NOT a failure, and retrying it double-sends. */+          if (!(res.status >= 200 && res.status < 300))+            throw new Error((j._hints && j._hints.note) || j.error || ('AI threads returned ' + res.status));+          /* PRE-1.0.187 SHAPE, kept because older Hydrogen builds still answer this way.+           * delivered_to is an ARRAY; a thread that opened but did not confirm delivery is+           * NOT a failure, and retrying it double-sends. */           var dt = j.delivered_to, arr = Array.isArray(dt) ? dt : (dt ? [dt] : []);           if (arr.length || (j.inject && j.inject.delivered_in_conversation)) return { state: 'delivered' };-          if (res.status >= 200 && res.status < 300) return { state: 'opened', hint: j._hints && j._hints.note };-          throw new Error((j._hints && j._hints.note) || j.error || ('AI threads returned ' + res.status));+          /* 1.0.187+ returns in well under a second with a JOB, not a delivery. ok:true means+           * the job STARTED. delivered_to is never filled on this path any more, so without+           * the poll below every send would report "delivery not confirmed" forever. */+          if (!j.job) return { state: 'opened', hint: j._hints && j._hints.note };+          var started = Date.now(), missing = false;+          return new Promise(function (resolve) {+            (function tick() {+              /* the deadline is checked in BOTH paths. Checking it only after a successful+               * poll leaves a dashboard whose server stops answering polling forever. */+              function again() {+                if (Date.now() - started > 120000)+                  return resolve({ state: 'opened', hint: 'no confirmation within 120s' });+                setTimeout(tick, 1000);+              }+              fetch(base + '/ai-threads/run-prompt/status?job=' + encodeURIComponent(j.job))+                .then(function (r) {+                  /* THE STATUS ROUTE IS A THIRD PROXY ROUTE. Dashboards proxy /updater/status+                   * and POST /ai-threads/run-prompt only. If this one was never added, every+                   * poll 404s and the user watches "Opening…" for the full 120s before a+                   * hedge, which is worse than the behaviour this replaced. Fail FAST and say+                   * exactly which route is missing instead of hanging. */+                  if (r.status === 404) { missing = true; return null; }+                  return r.json();+                })+                .then(function (st) {+                  if (missing) return resolve({ state: 'opened', phase: 'no_status_route',+                    hint: 'Your AI chat opened. This app does not proxy GET ' ++                          '/ai-threads/run-prompt/status yet, so progress cannot be shown.' });+                  var state = (st.job && st.job.state) || '';+                  if (typeof onProgress === 'function') { try { onProgress(state, st.job); } catch (e) {} }+                  /* working or later IS sent, for the user-facing toast. */+                  if (state === 'working' || state === 'done' || state === 'needs_you' ||+                      state === 'still_working') return resolve({ state: 'delivered', phase: state });+                  if (state === 'submit_failed' || state === 'submitted_unidentified' ||+                      state === 'submit_unconfirmed')+                    return resolve({ state: 'opened', phase: state,+                                     hint: (st.job && (st.job.error || st.job.note)) || state });+                  again();+                })+                .catch(again);+            })();+          });         })         .catch(function (e) {           clearTimeout(timer);@@ -316,7 +379,10 @@         if (sd.disabled) return;         sd.disabled = true;         var pending = toast('Opening a new AI thread…', 'info', 120000);-        send(base, phrase, appName, slug).then(function (r) {+        send(base, phrase, appName, slug, function (state, job) {+          if (PHASE[state]) pending.setText(PHASE[state]);+          if (typeof cfg.onProgress === 'function') { try { cfg.onProgress(state, job); } catch (e) {} }+        }).then(function (r) {           pending.remove();           if (r.state === 'delivered') toast('Sent. Your AI thread has the prompt.', 'ok');           else toast('Thread opened, delivery not confirmed. Check your AI threads before sending again.', 'info', 8000);@@ -412,6 +478,24 @@   function selfCheck(panel) {     panel = panel || document.querySelector('.anp');     if (!panel) return { ok: false, error: 'no .anp panel on the page' };+    /* REFUSE TO REPORT FROM A COLLAPSED LAYOUT. An unforegrounded tab returns zero-height+     * rects while computed styles still resolve normally, so selfCheck produced EIGHT GREEN+     * ROWS beside two red ones: "panel gutter: 0px left, 744px right" and "columns: want 1,+     * got 3". That mixed shape is far more dangerous than an all-zero one, because it reads+     * as a specific layout regression rather than an invalid run, and project-manager nearly+     * filed it as a bug. A refusal cannot be misread. (project-manager, 2026-08-25.) */+    var w0 = Math.round(panel.getBoundingClientRect().width);+    /* rows: [] is deliberate. The refusal previously omitted the key entirely, and EVERY+     * caller does r.rows.map(...) because every healthy result has rows, so a correct refusal+     * arrived at the caller as "Cannot read properties of undefined (reading 'map')" and read+     * like harness noise. kicad hit exactly that and logged it as its own crash. A guard whose+     * failure mode is a TypeError in someone else's stack is a guard that will be misread. */+    if (w0 === 0) return { ok: false, invalid: true, rows: [],+      error: 'panel measures 0px wide: this tab is not foregrounded (or the panel is hidden), ' ++             'so every layout row would ' ++             'be computed from a collapsed pane while the style rows pass normally. Activate ' ++             'the tab by name (or open the panel) and re-run. No rows reported, because a ' ++             'partial result from an unrendered pane looks like a real bug.' };     var cs = getComputedStyle(panel), rows = [], ok = true;     /* Chrome reports the USED border-width, snapped to whole device pixels, so a declared      * 1px reads as 0.666667px at dpr 1.5. Compare against the snapped value, not the@@ -507,6 +591,37 @@      * fleet mismatch comes back in a form the check is blind to. This asserts the panel's      * left edge against the PANE, which covers the whole ancestor chain in one number.      * (parts-search, 2026-08-25, reasoning about what the new yardstick still could not see.) */+    /* PHRASE HEADROOM AS A ROW, not a console warning nobody reads: "does not truncate TODAY"+     * and "has room" are different claims.+     * MEASURE need AGAINST AN OFFSCREEN nowrap PROBE. Do NOT use clientWidth - scrollWidth:+     * for a block span with overflow:hidden, scrollWidth is max(clientWidth, content), so that+     * expression SATURATES AT 0 for anything that fits, whether it fits by 1px or by 180px. It+     * still goes negative correctly when text overflows, so it looks like a working metric.+     * Two apps independently reported "0px headroom, one character from truncating" off it;+     * remeasured with this probe, neither had less than 141px of slack. A metric that can only+     * return one value on success is not evidence of success. */+    var spans = [].slice.call(panel.querySelectorAll('.anp-phrase span'));+    if (spans.length) {+      var probe = document.createElement('span');+      probe.style.cssText = 'position:absolute;visibility:hidden;white-space:nowrap;font:' ++                            getComputedStyle(spans[0]).font;+      document.body.appendChild(probe);+      var worst = 0, worstTxt = '', tight = 0;+      spans.forEach(function (sp) {+        probe.textContent = sp.textContent;+        var need = Math.ceil(probe.getBoundingClientRect().width);+        var have = Math.round(sp.getBoundingClientRect().width);+        var slack = have - need;+        if (worstTxt === '' || slack < worst) { worst = slack; worstTxt = sp.textContent.trim().slice(0, 40); }+        if (slack < 8) tight++;+      });+      probe.remove();+      var headOk = worst >= 8;+      if (!headOk) ok = false;+      rows.push({ what: 'phrase headroom', want: 'at least 8px of slack on every phrase',+                  got: worst + 'px on ' + JSON.stringify(worstTxt) + (tight > 1 ? ' (' + tight + ' tight)' : ''),+                  pass: headOk });+    }     var fam = getComputedStyle(panel.querySelector('.anp-phrase span') || panel).fontFamily;     var famOk = /Satoshi/.test(fam);     if (!famOk) ok = false;@@ -514,6 +629,63 @@     return { ok: ok, rows: rows };   } -  root.AdomNewbiePanel = { mount: mount, toast: toast, selfCheck: selfCheck, copyText: copyText, version: '2.8.0',+  /* checkSend(apiBase) - prove the send path EXISTS, without sending anything.+   * gate() fails OPEN on purpose, so a broken probe cannot block a user from sending. The+   * consequence, which kicad found in itself: A MISSING GATE READS AS AN ALLOWED GATE. Its+   * /updater/status route had been lost in a rebuild, the gate got HTML, caught, and returned+   * open, so the version gate had been silently no-opping. Nothing anywhere said so.+   * This makes that visible. It never posts a prompt: the run-prompt route is probed with+   * OPTIONS, so it can report "wired" without creating a thread in anyone's list. */+  function checkSend(base) {+    base = base == null ? '' : base;+    var rows = [], ok = true;+    function add(what, want, got, pass) { rows.push({what: what, want: want, got: got, pass: pass}); if (!pass) ok = false; }+    return fetch(base + '/updater/status', {cache: 'no-store'})+      .then(function (r) { return r.text().then(function (t) { return {s: r.status, t: t}; }); })+      .then(function (res) {+        var v = null, parsed = false;+        try { var j = JSON.parse(res.t); parsed = true; v = j && j.updater && j.updater.current_version; } catch (e) {}+        add('gate route wired', '200 with JSON', res.s + (parsed ? ' JSON' : ' NOT JSON'), res.s === 200 && parsed);+        add('gate reports a version', 'updater.current_version', String(v), !!v);+        if (v) add('version meets the minimum', MIN_API.join('.'), String(v), verGte(verNum(v), MIN_API));+      })+      .catch(function (e) { add('gate route wired', '200 with JSON', 'FETCH FAILED: ' + e.message, false); })+      .then(function () {+        /* PROBE A NONSENSE PATH TOO, AND REQUIRE THE TWO TO DIFFER.+         * "not 404" was too weak an assertion. kicad's do_OPTIONS returned 204 for EVERY path+         * unconditionally, so this row was green for any string at all, including routes that+         * exist in no method. An app could fix its GET catch-all, still have a blanket+         * preflight, and keep a green row that tests nothing. Comparing against a path that+         * cannot exist makes the row self-validating: if they answer identically, the server+         * is answering the SHAPE of the question rather than the question. */+        var nonsense = '/__anp_probe_' + String(Math.floor(1e9 * 0.6180339887)) + '__';+        return Promise.all([+          fetch(base + '/ai-threads/run-prompt', {method: 'OPTIONS'}).then(function (r) { return r.status; },+            function () { return 'ERR'; }),+          fetch(base + nonsense, {method: 'OPTIONS'}).then(function (r) { return r.status; },+            function () { return 'ERR'; })+        ]).then(function (both) {+          var real = both[0], fake = both[1];+          add('send route wired', 'answers, and differently from a path that cannot exist',+              'route ' + real + ', nonsense ' + fake,+              real !== 'ERR' && real !== 404 && real !== fake);+        });+      })+      .then(function () {+        /* THE STATUS ROUTE, added for Hydrogen 1.0.187+. It is a THIRD proxy route and no+         * dashboard had it before 2.22.0, so it is the row most likely to be red. A GET with+         * a job id that cannot exist must still be ANSWERED (any non-404), because a 404 here+         * means the route was never proxied and progress can never be reported. */+        return fetch(base + '/ai-threads/run-prompt/status?job=__anp_probe__')+          .then(function (r) { return r.status; }, function () { return 'ERR'; })+          .then(function (st) {+            add('progress route wired', 'answers (not 404)', String(st), st !== 'ERR' && st !== 404);+          });+      })+      .then(function () { return {ok: ok, rows: rows}; });+  }++  root.AdomNewbiePanel = { mount: mount, toast: toast, selfCheck: selfCheck, copyText: copyText,+                           checkSend: checkSend, version: '2.22.0',                            TWO_COL_MIN: TWO_COL_MIN, PHRASE_BUDGET: PHRASE_BUDGET }; })(typeof window !== 'undefined' ? window : this);
page.json+26−10
@@ -11,10 +11,6 @@     "uninstall": "./uninstall.sh"   },   "tags": [-    "ab",-    "bridge",-    "fusion",-    "desktop-app",     "fusion360",     "autodesk",     "cad",@@ -79,10 +75,7 @@     "build a basic parts library",     "add a 3d model to a component",     "make a fusion 360 library with 3d",-    "set up the fusion bridge",-    "fusion dashboard",-    "fusion status page",-    "open the fusion launch page"+    "set up the fusion bridge"   ],   "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.",@@ -111,5 +104,28 @@       "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
rust/Cargo.lock+5−5
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-fusion-bridge"-version = "1.9.368"+version = "1.9.369" dependencies = [  "base64",  "chrono",@@ -273,7 +273,7 @@ dependencies = [  [[package]] name = "fusion-core"-version = "1.9.368"+version = "1.9.369" dependencies = [  "serde",  "serde_json",@@ -283,7 +283,7 @@ dependencies = [  [[package]] name = "fusion-lbr"-version = "1.9.368"+version = "1.9.369" dependencies = [  "regex",  "roxmltree",@@ -295,14 +295,14 @@ dependencies = [  [[package]] name = "fusion-os-mac"-version = "1.9.368"+version = "1.9.369" dependencies = [  "fusion-core", ]  [[package]] name = "fusion-os-win"-version = "1.9.368"+version = "1.9.369" dependencies = [  "fusion-core",  "windows-sys 0.59.0",