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-control demo 2.0.53: machine labels on every run (resolved self name), fusion_state single-window rendering, picker self-dedupe
1 file changed
+57−16
web-control-demo.html+57−16@@ -66,7 +66,7 @@ <label for="target">Run on machine:</label> <select id="target"><option value="">this machine (self)</option></select> <button id="refresh-targets" style="background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:6px;padding:6px 10px;cursor:pointer;font-size:13px">Detect my computers</button>- <span class="pill" id="target-count">1 known</span>+ <span class="pill" id="target-count">self only until detected</span> </div> <div class="grp">Detect - open to any page, no permission prompt</div>@@ -165,6 +165,25 @@ for (const w of mine) { document.getElementById("origin").textContent = location.origin || "(null)"; function logLine(m) { log.textContent += "\n" + m; log.scrollTop = log.scrollHeight; } function chosenTarget() { return sel.value || null; }+ // WHO is "this machine": ab 2.0.53+ reports its relay client name at+ // /status.machine, so every card can say "on AdomLapper" instead of a vague+ // "this machine" (John: "always tell me which machine this demo is running on").+ var selfName = null;+ function loadSelfName() {+ return fetch(AB + "/status").then(function (r) { return r.json(); }).then(function (s) {+ var m = s && s.machine;+ if (m && (m.clientName || m.hostname)) selfName = m.clientName || m.hostname;+ return selfName;+ }).catch(function () { return null; });+ }+ loadSelfName();+ // The label for where a run lands. Verbs that pass no target run on SELF.+ function machineLabel(target) {+ if (target === undefined) target = chosenTarget();+ if (target === "all") return "ALL machines";+ return target || selfName || "this machine";+ }+ function selfLabel() { return selfName || "this machine"; } function setRes(el, cls, tag, detail) { el.className = "res " + cls; el.innerHTML = "<span class='tag'>" + tag + "</span>" + (detail ? " <small>" + detail + "</small>" : "");@@ -188,13 +207,14 @@ for (const w of mine) { function doVerb(el, command, args, target, summarize) { var body = { command: command }; if (args) body.args = args; if (target === undefined) target = chosenTarget(); if (target) body.target = target;- setRes(el, "pending", "asking permission...", "approve the Adom Bridge toast");- logLine("POST /command " + JSON.stringify(body));+ var who = machineLabel(target || null);+ setRes(el, "pending", "asking permission...", "on " + esc(who) + " - approve the Adom Bridge toast");+ logLine("POST /command " + JSON.stringify(body) + " [on " + who + "]"); fetch(AB + "/command", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }) .then(function (r) { return r.json().then(function (j) { return { s: r.status, j: j }; }); }) .then(function (res) { logLine(" <- HTTP " + res.s + " " + JSON.stringify(res.j).slice(0, 300));- if (res.s === 200) setRes(el, "ok", "Allowed & ran", summarize ? summarize(flat(res.j)) : "");+ if (res.s === 200) { var sum = summarize ? summarize(flat(res.j)) : ""; setRes(el, "ok", "Allowed & ran", "on " + esc(who) + (sum ? " - " + sum : "")); } else if (res.j && res.j.errorCode === "web_control_disabled") setRes(el, "err", "Refused: web control OFF", "re-enable in Settings → Web Control"); else setRes(el, "err", "Not approved", (res.j && (res.j.error || res.j.errorCode)) || ("HTTP " + res.s)); })@@ -371,7 +391,8 @@ 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”) ...", "the demo runs many steps - a 1-hour grant keeps the progress silent");+ setRes(c.res, "pending", "asking permission (choose “Allow 1 hr”) ...", "runs on " + esc(selfLabel()) + " - the demo runs many steps; a 1-hour grant keeps the progress silent");+ clog(c, "Target machine: " + selfLabel() + " (demos always run on 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). postVerb(cfg.startCmd, cfg.startArgs || {}).then(function (res) {@@ -451,6 +472,14 @@ for (const w of mine) { for (var i = 0; i < cands.length; i++) { if (Array.isArray(cands[i]) && cands[i].length) return cands[i]; }+ // fusion_state (1.9.42+) is a SINGLE-window server-side render: a top-level+ // base64 `image` (with imageAs:"base64") plus title/hwnd. Wrap it so the+ // one-window shape renders through the same path as the arrays.+ var dd = d.data || d;+ var single = dd.image || dd.imageBase64 || dd.screenshotBase64;+ if (typeof single === "string" && single.length > 100) {+ return [{ title: dd.title || "app window", image: single }];+ } return []; } // Render a bridge state payload's screenshots (each item: {title, image, ...}).@@ -475,7 +504,7 @@ 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"); });- setRes(c.res, "ok", withImg.length === 1 ? "Done - the surface is below" : "Demo done - full state below", withImg.length + " window(s)");+ setRes(c.res, "ok", withImg.length === 1 ? "Done - the surface is below" : "Demo done - full state below", withImg.length + " window(s) on " + esc(selfLabel())); c.btn.disabled = false; return true; }@@ -498,7 +527,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”) ...", cfg.note || "opens in the background, then we screenshot it");+ setRes(c.res, "pending", "asking permission (choose “Allow 1 hr”) ...", "on " + esc(selfLabel()) + " - " + (cfg.note || "opens in the background, then we screenshot it"));+ clog(c, "Target machine: " + selfLabel()); var staged = cfg.stage ? cfg.stage(c) : Promise.resolve(); staged.then(function () { var args = (typeof cfg.showArgs === "function") ? cfg.showArgs() : (cfg.showArgs || {});@@ -600,7 +630,7 @@ for (const w of mine) { // 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(", "); }- function targetsSummary(j) { var d = j.data || j; var a = d.targets || []; return (a.length || 1) + " machine(s): " + a.map(function (t) { return t.name || t.hostname; }).join(", "); }+ function targetsSummary(j) { var d = j.data || j; var a = d.targets || d.clients || []; return (a.length || 1) + " machine(s): " + a.map(function (t) { return t.name || t.hostname; }).join(", "); } function windowsSummary(j) { var d = j.data || j; var a = d.windows || []; return a.length + " open. Top 3: " + a.slice(0, 3).map(function (w) { return '"' + (w.title || "").slice(0, 26) + '"'; }).join(", "); } function browsersSummary(j) { var d = j.data || j; var a = d.browsers || (Array.isArray(d) ? d : []); if (!Array.isArray(a)) a = []; return a.length + " installed. Top 3: " + a.slice(0, 3).map(function (b) { return b.name || b.browser || b.displayName || b.id || "browser"; }).join(", "); } @@ -611,7 +641,8 @@ for (const w of mine) { fetch(AB + "/health").then(function (r) { return r.json(); }).then(function (h) { dot.style.background = (h && h.service === "adom-bridge") ? "#2ea043" : "#d29922"; return fetch(AB + "/status").then(function (r) { return r.json(); }).then(function (s) {- document.getElementById("ver").textContent = "v" + (s.version || "?");+ if (s.machine && (s.machine.clientName || s.machine.hostname)) selfName = s.machine.clientName || s.machine.hostname;+ document.getElementById("ver").textContent = "v" + (s.version || "?") + (selfName ? " on " + selfName : ""); var wc = (s && s.webControl) || {}; var el = document.getElementById("wc"); var on = wc.enabled !== false; el.textContent = on ? "ON" : "OFF"; el.style.color = on ? "#2ea043" : "#f85149"; setRes(cDetect.res, "ok", "Running: v" + (s.version || "?"), "web control " + (on ? "ON" : "OFF"));@@ -894,7 +925,7 @@ for (const w of mine) { var cShotSel = card("grp-multi", "multi", "Screenshot the SELECTED machine"); cShotSel.btn.onclick = function () { var sb = cShotSel.card.querySelector("div.shots"); if (sb) sb.innerHTML = "";- var who = chosenTarget() || "this machine";+ var who = machineLabel(); setRes(cShotSel.res, "pending", "asking permission...", "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); }@@ -906,14 +937,24 @@ for (const w of mine) { // ---- target picker ---- document.getElementById("refresh-targets").onclick = function () { logLine("POST /command {targets}");- fetch(AB + "/command", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ command: "targets" }) })+ // Resolve the self name first so the dedupe + "self" label are right even+ // when Detect was never clicked (an old ab just returns no machine block).+ (selfName ? Promise.resolve(selfName) : loadSelfName())+ .then(function () { return fetch(AB + "/command", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ command: "targets" }) }); }) .then(function (r) { return r.json(); }) .then(function (j) {- var d = j.data || j; var list = d.targets || [];- sel.innerHTML = '<option value="">this machine (self)</option>';- list.forEach(function (t) { var o = document.createElement("option"); o.value = t.name || t.hostname; o.textContent = (t.name || t.hostname) + (t.version ? " (v" + t.version + ")" : ""); sel.appendChild(o); });- if (list.length > 1) { var a = document.createElement("option"); a.value = "all"; a.textContent = "ALL machines (fan out)"; sel.appendChild(a); }- document.getElementById("target-count").textContent = (list.length || 1) + " known";+ // The direct API answers with clients[]; the relay-CLI shape is targets[].+ var d = j.data || j; var list = d.targets || d.clients || [];+ // The relay lists THIS machine by name too - that row is redundant with+ // the "self" option, so fold it in (it names the self option instead).+ var peers = list.filter(function (t) {+ var n = (t.name || t.hostname || "").toLowerCase();+ return !(selfName && n === selfName.toLowerCase());+ });+ sel.innerHTML = '<option value="">this machine (' + esc(selfLabel()) + ')</option>';+ peers.forEach(function (t) { var o = document.createElement("option"); o.value = t.name || t.hostname; o.textContent = (t.name || t.hostname) + (t.version ? " (v" + t.version + ")" : ""); sel.appendChild(o); });+ if (peers.length >= 1) { var a = document.createElement("option"); a.value = "all"; a.textContent = "ALL machines (fan out)"; sel.appendChild(a); }+ document.getElementById("target-count").textContent = selfLabel() + (peers.length ? " + " + peers.length + " more" : " only"); }) .catch(function (e) { logLine("targets failed: " + e); }); };