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
2.0.39: KiCad button drives kicad_demo (progress bar + shots)
1 file changed
+130−5
web-control-demo.html+130−5@@ -35,6 +35,10 @@ .shots { margin-top: 8px; display: flex; flex-direction: column; gap: 10px; } .shots .cap { font-size: 10px; color: #8b949e; margin-bottom: 3px; } .shots img { max-width: 100%; border: 1px solid #30363d; border-radius: 4px; display: block; }+ .pbar { margin: 8px 0 2px; height: 16px; background: #010409; border: 1px solid #21262d; border-radius: 8px; overflow: hidden; position: relative; }+ .pbar-fill { height: 100%; width: 0; background: linear-gradient(90deg, #1f6feb, #2ea043); transition: width .35s ease; }+ .pbar-meta { font-size: 11px; color: #9aa4af; margin-bottom: 4px; }+ .pbar-meta .beat { color: #6e7681; } .pill { font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid #30363d; color: #8b949e; } details { margin-top: 20px; } summary { cursor: pointer; color: #8b949e; font-size: 12px; } pre#log { background: #010409; border: 1px solid #21262d; border-radius: 6px; padding: 12px; font-size: 11px; max-height: 240px; overflow: auto; white-space: pre-wrap; margin: 8px 0 0; color: #c9d1d9; }@@ -326,6 +330,123 @@ for (const w of mine) { .catch(function (e) { setRes(c.res, "err", "blocked", String(e)); finish(); }); } + // ---- Bridge-OWNED demo verb: one long-running call, real progress bar ----+ // This is the modern pattern: the BRIDGE owns a "demo" verb that runs the whole+ // tour, reports structured progress (percent/stepLabel/beat/elapsed/estimated),+ // and returns its full window state as inline screenshots. The web page just+ // starts it and polls - no piecing bridge commands together, no title-guessing.+ // (The KiCad bridge shipped kicad_demo in 0.9.92; Fusion's fusion_demo is next.)+ function progressBar(card) {+ var box = card.querySelector("div.pbar");+ if (!box) {+ var meta = document.createElement("div"); meta.className = "pbar-meta"; meta.textContent = "starting ...";+ box = document.createElement("div"); box.className = "pbar";+ var fill = document.createElement("div"); fill.className = "pbar-fill"; box.appendChild(fill);+ // insert right after the .res line+ var res = card.querySelector("div.res");+ if (res && res.nextSibling) { card.insertBefore(meta, res.nextSibling); card.insertBefore(box, meta.nextSibling); }+ else { card.appendChild(meta); card.appendChild(box); }+ }+ return { fill: box.querySelector(".pbar-fill"), meta: card.querySelector(".pbar-meta") };+ }+ function setProgress(card, pct, label, beatIdx, beatCount, elapsed, estimate) {+ var p = progressBar(card);+ p.fill.style.width = Math.max(0, Math.min(100, pct || 0)) + "%";+ var beat = (beatCount ? ' <span class="beat">- step ' + Math.min((beatIdx || 0) + 1, beatCount) + "/" + beatCount + "</span>" : "");+ var timing = (elapsed != null && estimate != null) ? ' <span class="beat">- ' + elapsed + "s of ~" + estimate + "s</span>" : "";+ p.meta.innerHTML = (pct != null ? pct + "% " : "") + esc(label || "") + beat + timing;+ }+ function esc(s) { return String(s).replace(/[&<>]/g, function (c) { return { "&": "&", "<": "<", ">": ">" }[c]; }); }++ // Drive a bridge's demo verb: start (background) -> poll progress -> render the+ // returned window array. cfg = { startCmd, startArgs, pollArgs, stateCmd }.+ function runBridgeDemo(c, cfg) {+ c.btn.disabled = true;+ cardLog(c.card).textContent = "";+ var sb = c.card.querySelector("div.shots"); if (sb) sb.innerHTML = "";+ setProgress(c.card, 0, "starting ...");+ setRes(c.res, "pending", "asking permission (choose “Allow 1 hr”) ...", "the demo runs many steps - a 1-hour grant keeps the progress silent");+ clog(c, "Starting the bridge's own demo verb: " + cfg.startCmd + " " + JSON.stringify(cfg.startArgs || {}));+ // 1) Start the background tour (cross-origin -> ONE consent prompt).+ postVerb(cfg.startCmd, cfg.startArgs || {}).then(function (res) {+ if (res.s !== 200 || !res.j || (res.j.started !== true && res.j.success !== true)) {+ if (res.j && res.j.errorCode === "web_control_disabled") { setRes(c.res, "err", "Refused: web control OFF", "re-enable in Settings → Web Control"); }+ else { setRes(c.res, "err", "could not start", (res.j && (res.j.error || res.j.errorCode)) || ("HTTP " + res.s)); }+ c.btn.disabled = false; return;+ }+ var est = res.j.estimatedSec || 100;+ clog(c, "Started (bridge estimates ~" + est + "s). Polling for live progress ...");+ setProgress(c.card, res.j.percent || 2, "preparing ...", 0, 0, 0, est);+ setRes(c.res, "pending", "running the demo ...", "usually ~" + est + "s - watch the bar");+ // 2) Poll progress every 2.5s (the Allow-1hr grant makes these silent).+ var polls = 0, maxPolls = 240; // ~10 min ceiling+ function poll() {+ polls++;+ if (polls > maxPolls) { setRes(c.res, "err", "timed out", "the demo did not finish in time"); c.btn.disabled = false; return; }+ postVerb(cfg.pollCmd || cfg.startCmd, cfg.pollArgs || { progress: true }).then(function (pr) {+ if (pr.s !== 200 || !pr.j) { return new Promise(function (r) { setTimeout(r, 2500); }).then(poll); }+ var d = pr.j;+ if (d.error && d.active === undefined && d.done === undefined) {+ // job already gone - treat as done, go fetch the final state+ clog(c, " progress job ended; fetching final state ...");+ return finishDemo(c, cfg, est);+ }+ setProgress(c.card, d.percent, d.stepLabel || d.step, d.beatIndex, d.beatCount, d.elapsedSec, d.estimatedSec || est);+ if (d.stepLabel) clog(c, " " + (d.percent != null ? d.percent + "% " : "") + d.stepLabel + (d.beatCount ? " (step " + ((d.beatIndex || 0) + 1) + "/" + d.beatCount + ")" : ""));+ if (d.done === true || d.active === false) {+ setProgress(c.card, 100, "done", d.beatCount, d.beatCount, d.elapsedSec, d.estimatedSec || est);+ // The bridge rides its final state on the last poll (result.state.shots).+ // Render it if present; otherwise ask the state verb.+ if (!renderShots(c, d)) { return finishDemo(c, cfg, est); }+ return;+ }+ return new Promise(function (r) { setTimeout(r, 2500); }).then(poll);+ }).catch(function (e) { return new Promise(function (r) { setTimeout(r, 2500); }).then(poll); });+ }+ poll();+ }).catch(function (e) { setRes(c.res, "err", "blocked", String(e)); c.btn.disabled = false; });+ }+ // Pull the bridge's full state (the window array) and render it.+ function finishDemo(c, cfg, est) {+ if (!cfg.stateCmd) { setRes(c.res, "ok", "Demo finished", "the bridge closed its windows"); c.btn.disabled = false; return; }+ clog(c, "Fetching the bridge's full window state: " + cfg.stateCmd);+ return postVerb(cfg.stateCmd, {}).then(function (sr) {+ if (!(sr.s === 200 && sr.j && renderShots(c, sr.j))) {+ setRes(c.res, "ok", "Demo finished", "the bridge already closed its windows - nothing to show");+ clog(c, "DONE - the demo completed; the bridge closed its windows before we captured state.");+ c.btn.disabled = false;+ }+ }).catch(function (e) { setRes(c.res, "err", "state fetch failed", String(e)); c.btn.disabled = false; });+ }+ // Find the bridge's window array wherever it rides: the final poll embeds it at+ // result.state.shots; kicad_state returns it at shots. Handle every known path.+ function findShots(d) {+ var cands = [d.shots, d.screenshots, d.windows,+ d.data && d.data.shots, d.state && d.state.shots,+ d.result && d.result.state && d.result.state.shots,+ d.result && d.result.shots];+ for (var i = 0; i < cands.length; i++) {+ if (Array.isArray(cands[i]) && cands[i].length) return cands[i];+ }+ return [];+ }+ // Render a bridge state payload's screenshots (each item: {title, image, ...}).+ // Returns true if it rendered any window, false if there was nothing to show.+ function renderShots(c, d) {+ var narrate = (d.result && d.result.narrate) || d.narrate;+ var failed = (d.result && d.result.failedSteps) || d.failedSteps;+ if (narrate) clog(c, " bridge says: " + narrate);+ if (Array.isArray(failed) && failed.length) clog(c, " note: the bridge could not open: " + failed.join(", "));+ var shots = findShots(d);+ var withImg = shots.filter(function (s) { return s && (s.image || s.base64 || s.png); });+ if (!withImg.length) return false;+ clog(c, "Captured the bridge's full window state: " + withImg.length + " window(s).");+ withImg.forEach(function (s) { addShot(c.card, s.image || s.base64 || s.png, s.title || s.label || s.window || "window"); });+ setRes(c.res, "ok", "Demo done - full state below", withImg.length + " window(s)");+ c.btn.disabled = false;+ return true;+ }+ // summaries function n(x) { return (x === undefined || x === null) ? 0 : x; } function bridgesSummary(j) { var d = j.data || j; var a = d.bridges || []; return a.length + " bridge(s): " + a.map(function (b) { return b.name; }).slice(0, 6).join(", "); }@@ -361,12 +482,16 @@ for (const w of mine) { cApps.btn.onclick = function () { doVerb(cApps.res, "desktop_list_browsers", null, undefined, browsersSummary); }; // ---- Do something: stream the launch, then capture the FULL window array ----- var cKicad = card("grp-do", "verb", "Open KiCad (watched)");+ var cKicad = card("grp-do", "verb", "Run the KiCad demo (bridge-driven, live progress)"); cKicad.btn.onclick = function () {- streamRecipe(cKicad, [- { command: "kicad_open_symbol_editor", args: {}, label: "Launching KiCad", usuallySec: 30 },- { waitForWindow: { titleContains: "Symbol Editor", timeoutSec: 90 }, label: "Waiting for KiCad to come up", usuallySec: 8 }- ], { include: ["kicad", "symbol editor", "pcb editor", "schematic editor", "footprint editor"], exclude: ["for testing", "harness", "adom bridge web", "adom wiki"] });+ // The KiCad bridge OWNS this demo (kicad_demo, 0.9.92+): it builds a sample+ // project, opens the symbol/footprint/3D/board/schematic views, reports real+ // progress, and returns every window as a screenshot. We just start + poll.+ runBridgeDemo(cKicad, {+ startCmd: "kicad_demo", startArgs: { all: true, background: true },+ pollCmd: "kicad_demo", pollArgs: { progress: true },+ stateCmd: "kicad_state"+ }); }; var cFusion = card("grp-do", "verb", "Open Fusion 360"); cFusion.btn.onclick = function () {