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
web demo: bridge-agnostic reference implementation for rendering measured progress (readProgressBlock/renderProgressBlock), plus a card that animates an ORDINARY slow verb by polling alongside it
1 file changed
+251
web-control-demo.html+251@@ -40,6 +40,19 @@ .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; }+ /* Progress states - each encodes an honesty rule, see renderProgressBlock(). */+ /* Not "measured": the bridge is guessing, so it must not look as confident. */+ .pbar-fill.pbar-estimated { opacity: .5; }+ /* Parked on a HUMAN: amber, and it stops advancing. */+ .pbar-fill.pbar-blocked { background: linear-gradient(90deg, #9e6a03, #d29922); }+ .pbar-fill.pbar-failed { background: linear-gradient(90deg, #8b1a1a, #f85149); }+ /* Working, but the bridge cannot estimate: sweep instead of a fake 0%. */+ .pbar-fill.pbar-indeterminate {+ background: repeating-linear-gradient(90deg, #1f6feb 0 12px, #10305f 12px 24px);+ animation: pbar-sweep 1s linear infinite;+ }+ @keyframes pbar-sweep { from { background-position: 0 0; } to { background-position: 24px 0; } }+ @media (prefers-reduced-motion: reduce) { .pbar-fill.pbar-indeterminate { animation: none; } } .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; }@@ -450,6 +463,143 @@ for (const w of mine) { } function esc(s) { return String(s).replace(/[&<>]/g, function (c) { return { "&": "&", "<": "<", ">": ">" }[c]; }); } + // ==========================================================================+ // REFERENCE IMPLEMENTATION - rendering a bridge's measured progress on a web+ // page. THIS is the part to copy onto wiki.adom.inc.+ // --------------------------------------------------------------------------+ // Everything below is bridge-AGNOSTIC. It never mentions kicad or fusion, and+ // it works for a bridge that does not exist yet, because it matches on the+ // SHAPE of the measured-progress contract every bridge emits:+ //+ // { phase, stepLabel, percent, elapsedSec, estimatedSec, etaSec,+ // confidence, startKind, blockedBy?, measured? }+ //+ // Two functions, two jobs: readProgressBlock() gets the block out of whatever+ // the bridge answered with, renderProgressBlock() draws it. Keeping them apart+ // is deliberate - the reading half is where the traps are, the drawing half is+ // where the honesty rules are, and you will want to restyle one without+ // touching the other.+ // ==========================================================================++ // ---- 1. READ ------------------------------------------------------------+ // A bridge reports progress at TWO different moments, and a page that reads+ // only the first can show a bar that never moves:+ //+ // a) TERMINAL FRAME - a top-level `progress` on the slow verb's own+ // response. Always arrives at percent:100, because by the time the verb+ // answers, the work is finished. On its own this is a completion+ // receipt, NOT a progress bar.+ // b) LIVE POLL - an `active[]` array of those same blocks, one per+ // in-flight phase, from the bridge's fast progress verb while the slow+ // work is still running. THIS is the one that animates: poll it about+ // once a second and you get the stream of rising frames a bar needs.+ //+ // ⚠ THE TRAP: `active` is overloaded in the wild. The canonical contract uses+ // an ARRAY of blocks; some bridges' demo verbs use a BOOLEAN meaning+ // "still running". Disambiguate by TYPE, never by truthiness - `active:false`+ // and `active:[]` both mean idle, but `if (resp.active)` is true for a+ // non-empty array AND for the boolean, and false for BOTH idle forms, so a+ // truthiness test silently conflates a running job with a finished one.+ function readProgressBlock(resp) {+ if (!resp || typeof resp !== "object") return null;++ // Terminal frame wins when present: it is a verb's authoritative last word,+ // and it must not be overridden by a stale in-flight row.+ if (isProgressBlock(resp.progress)) return resp.progress;++ if (Array.isArray(resp.active)) {+ var rows = resp.active.filter(isProgressBlock);+ if (!rows.length) return null; // idle - draw nothing+ // Several phases in flight: draw the LEAST COMPLETE one. The honest answer+ // to "how far along is this?" is bounded by the slowest phase; drawing the+ // furthest-along one promises a finish the work has not earned. A row with+ // no percent sorts last so a row that HAS one wins.+ return rows.slice().sort(function (a, b) {+ return pctOf(a) - pctOf(b);+ })[0];+ }+ return null;+ }+ function pctOf(b) {+ return typeof b.percent === "number" ? b.percent : Infinity;+ }+ // A response can carry an unrelated field called `progress` (the toast+ // contract uses {title, value, status}), so a key name proves nothing.+ // Require a field only the measured-progress contract defines.+ function isProgressBlock(o) {+ return !!o && typeof o === "object" && !Array.isArray(o) &&+ ("percent" in o || "phase" in o || "etaSec" in o);+ }++ // ---- 2. RENDER ----------------------------------------------------------+ // Three honesty rules are baked in here on purpose. They are what separate a+ // progress bar the user can trust from one that is merely decorative:+ //+ // 1. An ESTIMATE IS MARKED AS ONE. `confidence` is the bridge telling you+ // how it got the number: "measured" means it timed this phase on THIS+ // machine, anything else is a default or a small sample. A guess that+ // renders identically to a measurement teaches the user to distrust+ // both, so a non-measured bar is drawn dimmed and labelled.+ // 2. `blockedBy` STOPS THE BAR. It means the work is parked on a HUMAN - a+ // modal dialog, a login, a save prompt. Nothing will move until the user+ // acts, so the bar goes amber, stops advancing, and says what it wants.+ // A bar that keeps sliding while nothing can happen is worse than none.+ // 3. NO PERCENT MEANS INDETERMINATE, not zero. A bridge that honestly says+ // "I am working but cannot estimate" must not render as a bar stuck at+ // 0%, which reads as "broken".+ function renderProgressBlock(card, b) {+ var p = progressBar(card);+ var blocked = typeof b.blockedBy === "string" && b.blockedBy.trim();+ var estimated = b.confidence && b.confidence !== "measured";+ var pct = typeof b.percent === "number"+ ? Math.max(0, Math.min(100, b.percent)) : null;++ p.fill.style.width = (pct === null ? 100 : pct) + "%";+ p.fill.classList.toggle("pbar-indeterminate", pct === null && !blocked);+ p.fill.classList.toggle("pbar-estimated", !!estimated && !blocked);+ p.fill.classList.toggle("pbar-blocked", !!blocked);+ p.fill.classList.toggle("pbar-failed", b.failed === true);++ // The human line: the bridge's own wording, never ours. `stepLabel` is+ // written for a person; `phase` is the machine-readable namespaced id and+ // is only a fallback.+ var bits = [];+ if (pct !== null) bits.push(Math.round(pct) + "%");+ var label = b.stepLabel || b.phase || "working";++ // Time: prefer what is LEFT (what a person actually wants) over what has+ // elapsed, and show the estimate it is measured against.+ if (typeof b.etaSec === "number" && b.etaSec >= 0) bits.push(secs(b.etaSec) + " left");+ else if (typeof b.elapsedSec === "number") {+ bits.push(secs(b.elapsedSec) + " elapsed" ++ (typeof b.estimatedSec === "number" ? " of ~" + secs(b.estimatedSec) : ""));+ }+ if (b.startKind) bits.push(b.startKind); // cold | warm+ if (estimated) bits.push(String(b.confidence)); // rule 1+ if (blocked) bits.push("waiting on you: " + b.blockedBy.trim()); // rule 2++ p.meta.innerHTML = esc(label) ++ (bits.length ? ' <span class="beat">- ' + esc(bits.join(" - ")) + "</span>" : "");+ return { blocked: !!blocked, percent: pct };+ }+ function secs(n) {+ if (typeof n !== "number" || !isFinite(n) || n < 0) return "";+ if (n < 60) return Math.round(n) + "s";+ var m = Math.floor(n / 60), s = Math.round(n % 60);+ return s ? m + "m " + s + "s" : m + "m";+ }++ // ---- 3. EVIDENCE --------------------------------------------------------+ // A progress frame may carry `stepShot {step, title, image}` - a screenshot+ // the bridge captured of the thing it just did. Render it verbatim as the+ // step's thumbnail: it is the difference between a bar claiming a footprint+ // opened and the user SEEING that it opened. `image` is a data: URL or bare+ // base64 PNG.+ function readStepShot(frameOrResp) {+ var f = frameOrResp || {};+ return f.stepShot || (f.result && f.result.stepShot) || null;+ }+ // Drive a bridge's demo verb: start (background) -> poll progress -> render the // returned window array. cfg = { startCmd, startArgs, pollArgs, stateCmd }. function runBridgeDemo(c, cfg) {@@ -801,6 +951,107 @@ 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 ----+ // ==========================================================================+ // THE CANONICAL PATTERN - drive ANY slow bridge verb and animate its progress.+ // --------------------------------------------------------------------------+ // The two cards above poll a bridge's purpose-built *_demo verb. This one does+ // NOT: it calls an ordinary slow verb, the kind a real page actually needs to+ // wait on, and animates it purely from the measured-progress contract. Nothing+ // below knows what KiCad is - swap the two verb names and it drives any bridge.+ //+ // The shape is the whole lesson, and it is not obvious the first time:+ //+ // fire the slow verb -------------------------------------> resolves (terminal frame)+ // poll <prefix>_progress every 1s --> --> --> --> --> stop+ //+ // TWO CONCURRENT REQUESTS. The slow verb's own response cannot animate+ // anything, because it does not arrive until the work is done - by then the+ // only honest number it can carry is 100%. The poll running ALONGSIDE it is+ // what produces the rising frames. Firing the verb and awaiting it before+ // starting the poll is the single most common way to end up with a bar that+ // jumps 0 -> 100 and nothing in between.+ // ==========================================================================+ function runCanonicalProgress(c, cfg) {+ var target = chosenTarget();+ var who = machineLabel(target);+ c.btn.disabled = true;+ cardLog(c.card).textContent = "";+ var sb = c.card.querySelector("div.shots"); if (sb) sb.innerHTML = "";+ setRes(c.res, "pending", "asking permission (choose “Allow 1 hr”) ...",+ "on " + esc(who) + " - one slow verb, animated purely from its progress block");++ var finished = false, frames = 0, sawMoving = false, lastLabel = null;++ // ---- the poll leg: this is what animates -------------------------------+ function poll() {+ if (finished) return;+ postVerb(cfg.pollCmd, cfg.pollArgs || {}, target).then(function (res) {+ if (finished) return;+ var block = readProgressBlock(res.j);+ if (block) {+ frames++;+ var st = renderProgressBlock(c.card, block);+ if (st.percent !== null && st.percent < 100) sawMoving = true;+ var label = block.stepLabel || block.phase;+ if (label && label !== lastLabel) {+ lastLabel = label;+ clog(c, " poll -> " + (block.percent != null ? block.percent + "% " : "") + label ++ (block.confidence ? " [" + block.confidence + "]" : "") ++ (block.blockedBy ? " BLOCKED: " + block.blockedBy : ""));+ }+ // A frame can carry its own evidence screenshot - show it verbatim.+ var ss = readStepShot(block) || readStepShot(res.j);+ if (ss && (ss.image || ss.base64)) addShot(c.card, ss.image || ss.base64,+ "step: " + (ss.step || ss.title || label || "step"));+ }+ setTimeout(poll, 1000);+ }).catch(function () { if (!finished) setTimeout(poll, 1000); });+ }++ // ---- the work leg: fire it, do NOT await before polling -----------------+ clog(c, "POST " + cfg.slowCmd + " (the slow verb; it will not answer until it is done)");+ clog(c, "POST " + cfg.pollCmd + " every 1s alongside it <- this is what moves the bar");+ var work = postVerb(cfg.slowCmd, cfg.slowArgs || {}, target);+ poll();++ work.then(function (res) {+ finished = true;+ // The TERMINAL frame: the verb's authoritative last word. Render it so the+ // bar lands on the real final numbers rather than wherever the last poll+ // happened to catch it.+ var block = readProgressBlock(res.j);+ if (block) {+ renderProgressBlock(c.card, block);+ clog(c, "terminal frame -> " + JSON.stringify(block));+ } else {+ clog(c, "terminal frame -> (this verb reported no progress block)");+ }+ var ok = res.s === 200 && res.j && res.j.success !== false;+ var note = frames + " live frame(s) polled" ++ (sawMoving ? ", bar animated" :+ ", none below 100% - this bridge does not populate active[] during this verb yet, " ++ "so the bar could only draw the terminal frame");+ setRes(c.res, ok ? "ok" : "err", ok ? "Allowed & ran" : "Failed",+ "on " + esc(who) + " - " + esc(note));+ c.btn.disabled = false;+ }).catch(function (e) {+ finished = true;+ setRes(c.res, "err", "blocked", String(e));+ c.btn.disabled = false;+ });+ }++ var cCanon = card("grp-do", "verb", "Animate ANY slow verb (the canonical pattern)");+ cCanon.btn.onclick = function () {+ // Ordinary verbs, not demo verbs. Swap these two names for any other bridge.+ runCanonicalProgress(cCanon, {+ slowCmd: "kicad_show_footprint",+ slowArgs: { footprintName: "R_0603_1608Metric", library: "Resistor_SMD" },+ pollCmd: "kicad_progress",+ pollArgs: {}+ });+ };+ var cKicad = card("grp-do", "verb", "Run the KiCad demo (bridge-driven, live progress)"); cKicad.btn.onclick = function () { // The KiCad bridge OWNS this demo (kicad_demo, 0.9.92+): it builds a sample