app
Adom Bridge
Public Made by Adomby adom
Adom Bridge unleashes your AI onto your full PC: full power, full safety. The AI breaks out of the container it runs in and onto your real machine, managing and running your entire laptop as you: launch apps, move files, drive any window, control KiCad, Fusion 360 or your real signed-in browser. Works with any AI, cloud or local (Claude Desktop, Claude Code, Codex), no Hydrogen required. Bridge and its bridges are an operating system for AI, with managed Node and Python runtimes, and a human-onl
← Commit history
demo: stop crying wolf about permission - claim nothing while a call is in flight, and name a permission problem only when the RESULT is one. Elapsed time cannot distinguish a 60s surface open from a pending prompt, so no threshold was ever going to work.
1 file changed
+110−12
web-control-demo.html+110−12@@ -348,18 +348,25 @@ for (const w of mine) { // away (that disclosure was fixed in ab 2.0.65), so the honest signal available // here is TIME - if the call has not come back, a human is being asked. function setAwaiting(el, promptTag, promptDetail, runningDetail) {+ // Say "running", and NOTHING about permission.+ //+ // Two earlier attempts got this wrong in the same direction. First every card+ // announced "asking permission" the instant it started, which is a lie in Auto+ // mode where no prompt ever appears. Then I escalated to that copy if the call+ // was still outstanding after 1.2s - but a surface open legitimately runs 30-60+ // SECONDS, so a slow verb looks exactly like a pending prompt and the lie came+ // back. Elapsed time cannot tell those apart, so no threshold will ever fix it.+ //+ // A page cannot see its own grant either: a cross-origin /status deliberately+ // projects the grants list away (ab#89). So the honest thing is to claim+ // nothing while the call is in flight, and speak only when the RESULT tells us+ // something real - which the failure paths now do, naming a permission problem+ // only when the response actually is one. el._promptShown = false; el.className = "res running"; el.innerHTML = "<span class='tag'>running ...</span>" + (runningDetail ? " <small>" + runningDetail + "</small>" : "");- var token = (el._awaitToken = (el._awaitToken || 0) + 1);- setTimeout(function () {- if (el._awaitToken !== token) return; // already resolved - no prompt happened- el._promptShown = true;- el.className = "res pending";- el.innerHTML = "<span class='tag'>" + promptTag + "</span>" +- (promptDetail ? " <small>" + promptDetail + "</small>" : "");- }, 1200);+ el._awaitToken = (el._awaitToken || 0) + 1; } // Most desktop verbs return {success, output:"<stringified json>"}. Parse output up. function flat(j) {@@ -924,6 +931,24 @@ for (const w of mine) { } clog(c, "Rendering " + withImg.length + " window(s)."); withImg.forEach(function (s) { addShot(c.card, s.image || s.base64 || s.png, s.title || s.label || s.window || "window"); });+ // Is what we are about to render actually the part that was ASKED for?+ // The window title carries the part name, so this is cheap - and it is the+ // check that would have caught a green "Done" printed over screenshots of+ // the previous part (John, 2026-08-20) instead of shipping it as success.+ if (c._verifyName) {+ var want = norm(c._verifyName);+ var titles = withImg.map(function (s) { return (s.title || s.label || s.window || "").toString(); });+ if (titles.length && !titles.some(function (t) { return norm(t).indexOf(want) >= 0; })) {+ withImg.forEach(function (s) { addShot(c.card, s.image || s.base64 || s.png, "MISMATCH: " + (s.title || s.label || "window")); });+ setRes(c.res, "err", "WRONG PART on screen",+ "asked for " + esc(c._verifyName) + " but the surface shows " + esc(titles.slice(0, 2).join(" | ")) ++ " - the evidence below is NOT this part");+ clog(c, " MISMATCH: no rendered window mentions '" + c._verifyName + "' -> " + titles.join(" | "));+ c.btn.disabled = false;+ return true;+ }+ }+ if (typeof c._onShown === "function") { try { c._onShown(); } catch (e) {} } setRes(c.res, "ok", withImg.length === 1 ? "Done - the surface is below" : "Demo done - full state below", withImg.length + " window(s) on " + esc(c._who || selfLabel())); c.btn.disabled = false; return true;@@ -1008,8 +1033,15 @@ for (const w of mine) { : { percent: 100, stepLabel: "stopped", failed: true }); } + // Normalize for comparison: a part's name differs in case and punctuation+ // across the wiki slug, the library entry and the window title+ // (IN-S42ATR / in-s42atr, s10b-ph-sm4-tb / S10B_PH_SM4_TB).+ function norm(x) { return String(x || "").toLowerCase().replace(/[^a-z0-9]/g, ""); }+ function showSurface(c, cfg) { c.btn.disabled = true;+ c._verifyName = cfg.verifyName || null;+ c._onShown = cfg.onShown || null; cardLog(c.card).textContent = ""; var sb = c.card.querySelector("div.shots"); if (sb) sb.innerHTML = ""; setAwaiting(c.res, "asking permission (choose “Allow 1 hr”) ...", "on " + esc(selfLabel()) + " - " + (cfg.note || "opens in the background, then we screenshot it"), "on " + esc(selfLabel()) + " - " + (cfg.note || "opens in the background, then we screenshot it"));@@ -1031,7 +1063,18 @@ for (const w of mine) { return handleShow(res); }); function handleShow(res) {- var ok = res.s === 200 && res.j && (res.j.success === true || res.j.ok === true || res.j.surface || res.j.shots || res.j.result || res.j.state);+ // An EXPLICIT failure is a failure, full stop. This used to OR together+ // several optional fields (`state`, `result`, `shots`, ...) as evidence+ // of success - but a kicad ERROR response also carries `state`, so a+ // refusal like library_cache_stale scored as OK and the card printed a+ // green "Done" over screenshots of the PREVIOUS part. Believing a bridge+ // when it says success:false is the whole fix; the optional fields are a+ // fallback only for a bridge that omits `success` entirely.+ var jj = res.j || {};+ var explicitFail = jj.success === false || !!jj.errorCode ||+ (typeof jj.error === "string" && jj.error.trim() !== "");+ var ok = res.s === 200 && !explicitFail &&+ (jj.success === true || jj.ok === true || jj.surface || jj.shots || jj.result || jj.state); // The host app may simply not be running yet (fusion_not_running). If the // bridge names a start verb, run it (it blocks until ready) and retry ONCE. if (!ok && cfg.startCmd && !c._autoStarted && res.j && /not running/i.test((res.j.error || "") + " " + (res.j._hint || ""))) {@@ -1049,7 +1092,15 @@ for (const w of mine) { } if (!ok) { if (res.j && res.j.errorCode === "web_control_disabled") setRes(c.res, "err", "web control OFF", "re-enable in Settings → Web Control");+ else {+ // Name a permission failure for what it is - this is the ONLY moment the+ // page actually knows one happened.+ var em = ((res.j && (res.j.error || res.j.errorCode)) || "") + "";+ var perm = res.s === 403 || /not approved|denied|consent|permission/i.test(em);+ if (perm) setRes(c.res, "err", "needs your approval",+ "Adom Bridge asked and got no Allow - click Allow 1 hr on the prompt (check the notification center, Win+N) then run this again"); else setRes(c.res, "err", "not opened", (res.j && (res.j.error || res.j.errorCode || res.j._hint)) || ("HTTP " + res.s));+ } c.btn.disabled = false; return; } clog(c, " opened" + (res.j.surface ? " (surface: " + res.j.surface + ")" : "") + " - capturing state ...");@@ -1330,6 +1381,7 @@ for (const w of mine) { // Cards whose LABEL names the part, so switching parts re-renders them. var PART_CARDS = [];+ var KICAD_SHOWN = null; // last part actually shown in a KiCad editor function fmtKB(n) { return n >= 1048576 ? (n / 1048576).toFixed(2) + " MB" : Math.round(n / 1024) + " KB"; } @@ -1340,6 +1392,7 @@ for (const w of mine) { function setPart(idx) { PART = partFrom(PARTS[idx]); _fusionPart = null;+ _kicadPart = null; // re-stage: symbol name, library and paths are per-part PART_CARDS.forEach(function (e) { e.cc.btn.textContent = e.label(); setRes(e.cc.res, "idle", "not run yet");@@ -1475,6 +1528,16 @@ for (const w of mine) { return postVerb("kicad_install_symbol", { filePath: paths[0], quietInstall: true }).then(function (r) { if (!(r.s === 200 && r.j && r.j.success)) throw stageFail("kicad_install_symbol: " + ((r.j && r.j.error) || r.s)); if (r.j.libraryName) PART.kicadLib = r.j.libraryName;+ // The symbol's name INSIDE the .kicad_sym is often NOT the filename:+ // tja1051t-3-118 declares "TJA1051T_3,118", type-c-31-m-12 declares+ // "USB_C_Receptacle_USB2.0_16P", CL21B104KBCNFNC drops its last letter.+ // Asking for the file stem makes the bridge correctly fail to find it -+ // 4 of 10 parts in the picker. The bridge reports the real name here, so+ // use it for the show call AND for the wrong-part check.+ if (r.j.symbolNames && r.j.symbolNames.length) {+ PART.kicadSymbolName = r.j.symbolNames[0];+ if (PART.kicadSymbolName !== PART.name) clog(c, " symbol is named '" + PART.kicadSymbolName + "' inside the file (not '" + PART.name + "')");+ } clog(c, " installed symbol into the '" + PART.kicadLib + "' library"); return postVerb("kicad_install_footprint", { filePath: paths[1], model: { filePath: paths[2] }, quietInstall: true }); }).then(function (r) {@@ -1518,7 +1581,7 @@ for (const w of mine) { // ALL EIGHT open cold + background (the 3D warm-only limitation is obsolete: // kicad-bridge #26 closed via the Win32 menu tier in 0.9.99). [- { label: function () { return "Symbol (" + PART.name + ")"; }, cmd: "kicad_show_symbol", args: function () { return { symbolName: PART.name, libraryName: PART.kicadLib }; }, stage: stageKicadPart, pick: "symbol editor", note: "installs the part from its wiki page, then shows JUST the symbol" },+ { label: function () { return "Symbol (" + PART.name + ")"; }, cmd: "kicad_show_symbol", args: function () { return { symbolName: PART.kicadSymbolName || PART.name, libraryName: PART.kicadLib }; }, stage: stageKicadPart, pick: "symbol editor", note: "installs the part from its wiki page, then shows JUST the symbol" }, { label: function () { return "Footprint (" + PART.name + ")"; }, cmd: "kicad_show_footprint", args: function () { return { footprintName: PART.name, library: PART.kicadLib }; }, stage: stageKicadPart, pick: "footprint editor", note: "just the land pattern: pads + courtyard" }, { label: function () { return "3D chip (" + PART.name + ")"; }, cmd: "kicad_show_3d_chip", args: function () { return { footprintName: PART.name, library: PART.kicadLib }; }, stage: stageKicadPart, pick: "3d viewer", note: "the part's STEP model on its footprint" }, { label: function () { return "Library (" + PART.name + " in '" + PART.kicadLib + "')"; }, cmd: "kicad_show_library", args: function () { return { libraryName: PART.kicadLib }; }, stage: stageKicadPart, pick: "symbol editor", note: "the library holding the symbol + footprint + 3D model" },@@ -1529,7 +1592,38 @@ for (const w of mine) { ].forEach(function (s) { var cc = card("grp-kicad", "verb", labelOf(s)); PART_CARDS.push({ cc: cc, label: labelFnOf(s) });- cc.btn.onclick = function () { showSurface(cc, { showCmd: s.cmd, showArgs: s.args, stage: s.stage, pickTitle: s.pick, stateCmd: "kicad_state", note: s.note }); };+ cc.btn.onclick = function () {+ showSurface(cc, {+ showCmd: s.cmd,+ // A KiCad editor indexes its libraries when it OPENS, so after switching+ // parts the already-open editor cannot see the new symbol and keeps+ // showing the previous one (errorCode library_cache_stale). Ask the+ // bridge to close and reopen it - but ONLY when the part actually+ // changed, because the reopen pays the cold library index (~1 min,+ // kicad-bridge #35).+ showArgs: function () {+ var a = (typeof s.args === "function") ? s.args() : (s.args || {});+ // Rescan unless we JUST showed this exact part. On the first open we+ // cannot know what the already-open editor holds, and assuming it is+ // right is precisely how a stale editor gets reported as success.+ if (KICAD_SHOWN !== PART.name) a.rescan = true;+ return a;+ },+ stage: s.stage, pickTitle: s.pick, stateCmd: "kicad_state", note: s.note,+ // Verify against the name we ACTUALLY asked for, which for a symbol is+ // the in-file name, not the file stem.+ // WHAT can be verified by window title differs per surface:+ // symbol -> the in-file symbol name (often NOT the file stem)+ // footprint -> the part name+ // library -> NOTHING: the window shows the LIBRARY, and its title+ // is whichever symbol the editor landed on, so demanding+ // the part name there is a false alarm, not a catch.+ verifyName: s.cmd === "kicad_show_library" ? null+ : s.cmd === "kicad_show_symbol" ? (PART.kicadSymbolName || PART.name)+ : PART.name,+ onShown: function () { KICAD_SHOWN = PART.name; }+ });+ }; }); var cKState = card("grp-kicad", "read", "Show KiCad state (every window)"); cKState.btn.onclick = function () { showSurface(cKState, { showCmd: "kicad_state", showArgs: { maxWidth: 900 }, stateCmd: "kicad_state", note: "captures every KiCad window as a screenshot" }); };@@ -1596,7 +1690,11 @@ for (const w of mine) { PART_CARDS.push({ cc: cc, label: labelFnOf(s) }); cc.btn.onclick = function () { // fallback stays a FUNCTION: its args reference paths the stage step produces.- showSurface(cc, { showCmd: s.cmd, showArgs: s.args, stage: s.stage, fallback: s.fallback, pickTitle: "fusion", stateCmd: "fusion_state", startCmd: "fusion_start", windowInfoCmd: "fusion_window_info", note: s.note });+ showSurface(cc, { showCmd: s.cmd, showArgs: s.args, stage: s.stage, fallback: s.fallback, pickTitle: "fusion", stateCmd: "fusion_state", startCmd: "fusion_start", windowInfoCmd: "fusion_window_info", note: s.note,+ // Fusion DESIGN surfaces (schematic / board / project) open a generated+ // design whose window is not named after the part, so only the COMPONENT+ // surfaces can be verified by title.+ verifyName: s.verifiable === false ? null : PART.name }); }; }); // Single full-screen shot (keep the simple case too).