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: fluid layout (was pinned 1000px, wasting ~840px on a wide window), scrollbar to the true right edge (stable not both-edges), progress bars land on a final frame instead of freezing mid-run, and no orange permission alarm when a standing grant means no prompt ever appears
1 file changed
+84−13
web-control-demo.html+84−13@@ -9,7 +9,21 @@ <style> :root { color-scheme: dark; } html, body { margin: 0; padding: 0; background: #0d1117; color: #e6edf3; font-family: system-ui, "Segoe UI", sans-serif; }- .wrap { max-width: 1000px; margin: 0 auto; padding: 24px; }+ /* Border-box everywhere so padding can never push a child past the viewport+ and create a page-level horizontal scrollbar (always a bug, never a feature). */+ *, *::before, *::after { box-sizing: border-box; }++ /* FLUID column. It was pinned at 1000px, which on a 1900px window left ~420px+ of dead space on EACH side and made the page look like it was ignoring the+ window. It now grows with the viewport up to a readable ceiling, and the+ .cards grid (auto-fill minmax) turns that extra width into more columns+ instead of longer lines. Padding scales with the viewport too. */+ .wrap {+ width: 100%;+ max-width: 1760px;+ margin: 0 auto;+ padding: 24px clamp(16px, 2.5vw, 40px);+ } /* ── Scrollbars: the Adom pretty one, per adom-ui-design/skills/scrollbars ── A default OS scrollbar in an Adom app is hideous (John, 2026-08-18). The@@ -18,10 +32,12 @@ /* Firefox + the standard property */ * { scrollbar-width: thin; scrollbar-color: rgba(139,148,158,.38) transparent; } - /* The centered .wrap is exactly the case the skill calls out: reserve the- gutter on BOTH edges so the column does not jump sideways the moment a- scrollbar appears. */- html { scrollbar-gutter: stable both-edges; }+ /* `stable` (NOT `both-edges`): stable alone reserves the scrollbar's own gutter+ so the layout never jumps when it appears. `both-edges` additionally reserves+ a MATCHING gutter on the opposite side, which holds the scrollbar visibly off+ the right edge of the window - John caught that immediately. The centering+ argument for both-edges does not apply once the column is fluid. */+ html { scrollbar-gutter: stable; } /* Chromium / WebKit */ ::-webkit-scrollbar { width: 10px; height: 10px; }@@ -73,6 +89,7 @@ .res { font-size: 12px; line-height: 1.35; min-height: 16px; color: #8b949e; word-break: break-word; } .res .tag { font-weight: 600; } .res.idle { color: #6e7681; }+ .res.running { color: #8b949e; } /* neutral: started, nothing to worry about */ .res.pending { color: #d29922; } .res.ok { color: #3fb950; } .res.err { color: #f85149; }@@ -302,9 +319,41 @@ for (const w of mine) { } function selfLabel() { return selfName || "this machine"; } function setRes(el, cls, tag, detail) {+ // Any result CANCELS a queued "asking permission" escalation (see setAwaiting).+ el._awaitToken = (el._awaitToken || 0) + 1;+ // If this run never had to ask, say so. A standing grant is invisible+ // otherwise, and the user is left guessing whether approval happened.+ if (cls === "ok" && el._promptShown === false) {+ detail = (detail ? detail + " - " : "") + "auto-approved (no prompt needed)";+ } el.className = "res " + cls; el.innerHTML = "<span class='tag'>" + tag + "</span>" + (detail ? " <small>" + detail + "</small>" : ""); }++ // Start a run NEUTRALLY, and only claim we are waiting on a human if we still+ // are after a beat.+ //+ // Every card used to announce "asking permission (choose Allow 1 hr)" in ORANGE+ // the instant it started - even when the origin was already on Auto mode, where+ // no prompt ever appears and the call returns in milliseconds. That reads as a+ // permission PROBLEM when there is none (John, 2026-08-20). The page cannot+ // simply read the grant list: a cross-origin /status deliberately projects it+ // 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) {+ 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);+ } // Most desktop verbs return {success, output:"<stringified json>"}. Parse output up. function flat(j) { if (j && typeof j.output === "string" && j.output.charAt(0) === "{") {@@ -326,7 +375,7 @@ for (const w of mine) { if (target === undefined) target = chosenTarget(); if (target) body.target = target; if (target) { body.args = body.args || {}; if (!body.args.caller) body.args.caller = { aiThread: "web-control demo page", containerName: selfLabel() }; } var who = machineLabel(target || null);- setRes(el, "pending", "asking permission...", "on " + esc(who) + " - approve the Adom Bridge toast (no banner? Windows may have tucked it in the notification center - press Win+N)");+ setAwaiting(el, "asking permission...", "on " + esc(who) + " - approve the Adom Bridge toast (no banner? Windows may have tucked it in the notification center - press Win+N)", "on " + esc(who)); logLine("POST /command " + JSON.stringify(body) + " [on " + who + "]"); postRaw(body) .then(function (r) { return r.json().then(function (j) { return { s: r.status, j: j }; }); })@@ -683,7 +732,7 @@ for (const w of mine) { 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”) ...", "runs on " + esc(who) + " - the demo runs many steps; a 1-hour grant keeps the progress silent");+ setAwaiting(c.res, "asking permission (choose “Allow 1 hr”) ...", "runs on " + esc(who) + " - the demo runs many steps; a 1-hour grant keeps the progress silent", "runs on " + esc(who)); clog(c, "Target machine: " + who + (target ? " (relayed through this machine's Adom Bridge)" : " (the machine serving this page)")); 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).@@ -910,11 +959,12 @@ for (const w of mine) { // The card disables its button for the whole run and re-enables it on // EVERY exit path, so that flag is the one stop condition that cannot // leave this loop polling forever.- if (!c.btn.disabled) return;+ if (!c.btn.disabled) { finishSurfaceBar(c); return; } postVerb(pollCmd, {}).then(function (res) { if (!c.btn.disabled) return; var block = readProgressBlock(res.j); if (block) {+ c._sawProgress = true; renderProgressBlock(c.card, block); var ss = readStepShot(block); var img = ss && (ss.image || ss.base64);@@ -931,11 +981,31 @@ for (const w of mine) { })(); } + // Land the bar on a FINAL frame when the run ends.+ //+ // The poll loop stops the moment the card re-enables its button, and it used to+ // just return - leaving the bar frozen wherever the last poll happened to catch+ // it. That produced the thing John caught: a card reading "Done - the surface is+ // below" in green above a bar still saying "show_footprint starting - 23% - 3s+ // left". The verb finished between polls, so the last frame was mid-flight and+ // nothing ever superseded it.+ //+ // The outcome is read from the card's own result class rather than assumed, so a+ // FAILED run does not get a triumphant 100% - it lands red and says stopped.+ function finishSurfaceBar(c) {+ if (!c._sawProgress) return; // no bar was ever drawn; nothing to land+ c._sawProgress = false; // once per run+ var ok = c.res && c.res.classList && c.res.classList.contains("ok");+ renderProgressBlock(c.card, ok+ ? { percent: 100, stepLabel: "done", etaSec: 0 }+ : { percent: 100, stepLabel: "stopped", failed: true });+ }+ function showSurface(c, cfg) { 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(selfLabel()) + " - " + (cfg.note || "opens in the background, then we screenshot it"));+ 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")); clog(c, "Target machine: " + selfLabel()); startSurfaceProgress(c, cfg.showCmd); var staged = cfg.stage ? cfg.stage(c) : Promise.resolve();@@ -1089,7 +1159,8 @@ for (const w of mine) { 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”) ...",+ setAwaiting(c.res, "asking permission (choose “Allow 1 hr”) ...",+ "on " + esc(who) + " - one slow verb, animated purely from its progress block", "on " + esc(who) + " - one slow verb, animated purely from its progress block"); var finished = false, frames = 0, sawMoving = false, lastLabel = null;@@ -1397,7 +1468,7 @@ for (const w of mine) { var cShot = card("grp-do", "verb", "Take a screenshot (whole screen)"); cShot.btn.onclick = function () { var sb = cShot.card.querySelector("div.shots"); if (sb) sb.innerHTML = "";- setRes(cShot.res, "pending", "asking permission...", "approve the toast");+ setAwaiting(cShot.res, "asking permission...", "approve the toast", "capturing the screen"); postVerb("desktop_screenshot_base64", { maxWidth: 900 }, chosenTarget()).then(function (res) { if (res.s === 200 && res.j.image) { setRes(cShot.res, "ok", "Captured " + res.j.width + "x" + res.j.height, ""); addShot(cShot.card, res.j.image, "whole screen"); } else if (res.j && res.j.errorCode === "web_control_disabled") setRes(cShot.res, "err", "Refused: web control OFF", "re-enable in Settings");@@ -1416,7 +1487,7 @@ for (const w of mine) { var notifyId = null; cNotify.btn.onclick = function () { notifyId = "webdemo-" + (Math.floor(Math.random() * 1e9));- setRes(cNotify.res, "pending", "asking permission...", "approve the toast");+ setAwaiting(cNotify.res, "asking permission...", "approve the toast", "sending the toast"); postVerb("notify_user", { title: "Hello from a web page", body: "Sticky toast. Click Got it or Dismiss, then Check toast state.", scenario: "reminder", id: notifyId, buttons: [{ label: "Got it" }, { label: "Dismiss" }] }, chosenTarget()).then(function (res) { var act = res.j && res.j.action; if (res.s === 200 && act && String(act).toLowerCase().indexOf("error") === 0) { setRes(cNotify.res, "err", "notify rejected", String(act).slice(0, 140)); return; }@@ -1451,7 +1522,7 @@ for (const w of mine) { cShotSel.btn.onclick = function () { var sb = cShotSel.card.querySelector("div.shots"); if (sb) sb.innerHTML = ""; var who = machineLabel();- setRes(cShotSel.res, "pending", "asking permission...", "capturing " + who);+ setAwaiting(cShotSel.res, "asking permission...", "capturing " + who, "capturing " + who); postVerb("desktop_screenshot_base64", { maxWidth: 900 }, chosenTarget()).then(function (res) { if (res.s === 200 && res.j.image) { setRes(cShotSel.res, "ok", "Captured " + res.j.width + "x" + res.j.height, "on " + who); addShot(cShotSel.card, res.j.image, who); } else if (res.j && res.j.errorCode === "web_control_disabled") setRes(cShotSel.res, "err", "Refused: web control OFF", "re-enable in Settings");