← Commit history
Publish 0.15.0
4 files changed
+232−15
bin/nb-dashboard+1−1@@ -20,7 +20,7 @@ const { execFile, spawn } = require('child_process'); const fs = require('fs'); const crypto = require('crypto'); -const VERSION = '0.14.0'; // keep in lockstep with package.json (pkg version)+const VERSION = '0.15.0'; // keep in lockstep with package.json (pkg version) // SEND TO AI IS UN-PARKED (John: "yeah, we are re-enabling the send to ai. it seems to be good now"). // The ah build thread certified the crash family dead on the gate-fixed build and the delivery leg is // proven end to end. The park const is DELETED rather than flipped, so no dead branch can resurrect it.
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "nb-bridge",- "version": "0.14.0",+ "version": "0.15.0", "type": "app", "title": "NB - Native Browser Bridge", "description": "This package installs the SKILLS into your container so your AI knows how to drive your real signed-in Chrome and Edge; Bridge loads the bridge runtime itself from the release zip.",
page.json+76−6@@ -16,10 +16,10 @@ "chrome", "edge", "nbrowser",- "ai-agent",- "developer-tools",- "vibe-coding",- "native-messaging"+ "pup",+ "native-messaging",+ "credentials",+ "chip-fetcher" ], "status": "live", "discovery_triggers": [@@ -70,5 +70,75 @@ }, "visibility": "public", "brief": "Your real browser, driven by AI.",- "discovery_pitch": "nbe (the Adom Native Browser Extension, layer 2 of nb) drives the user's REAL signed-in Chrome/Edge via nbrowser_* verbs: use it for LOGGING IN, filling forms, and downloading behind auth with their OWN saved passwords - pup cannot (it is a fresh Chrome-for-Testing with no logins). Next moves: run 'adom-bridge bridge_list' to check it is installed; if not, OFFER to install it (see the installing-the-extension skill) - never make the user do it. If this container lacks the skills, 'adom-wiki pkg install adom/nb-bridge'. Check readiness with 'adom-bridge nbrowser_readiness' (state:needs-extension means offer the install)."-}+ "discovery_pitch": "nbe (the Adom Native Browser Extension, layer 2 of nb) drives the user's REAL signed-in Chrome/Edge via nbrowser_* verbs: use it for LOGGING IN, filling forms, and downloading behind auth with their OWN saved passwords - pup cannot (it is a fresh Chrome-for-Testing with no logins). Next moves: run 'adom-bridge bridge_list' to check it is installed; if not, OFFER to install it (see the installing-the-extension skill) - never make the user do it. If this container lacks the skills, 'adom-wiki pkg install adom/nb-bridge'. Check readiness with 'adom-bridge nbrowser_readiness' (state:needs-extension means offer the install).",+ "version": "0.15.0",+ "type": "app",+ "description": "This package installs the SKILLS into your container so your AI knows how to drive your real signed-in Chrome and Edge; Bridge loads the bridge runtime itself from the release zip.",+ "install": {+ "binary_name": "nb-bridge"+ },+ "files": [+ "screenshots/hero.png",+ "skills/adom-native-browser/SKILL.md",+ "skills/driving-sites/SKILL.md",+ "skills/driving-the-desktop/SKILL.md",+ "skills/driving-the-extension/SKILL.md",+ "skills/driving-the-extension-demo/SKILL.md",+ "skills/installing-the-extension/SKILL.md",+ "skills/native-browser-recording/SKILL.md",+ "skills/native-browser-safety/SKILL.md",+ "skills/reading-google-messages/SKILL.md",+ "skills/uninstalling-the-extension/SKILL.md",+ "bin/nb-dashboard",+ "docs/img/dock-nb.svg",+ "docs/img/settings/overlay-on.png",+ "docs/img/settings/overlay-off.png",+ "docs/img/settings/activity-progress.png",+ "docs/img/settings/activity-flash.png",+ "docs/img/settings/activity-off.png",+ "demo/audio/intro.mp3",+ "demo/audio/open.mp3",+ "demo/audio/shot.mp3",+ "demo/audio/click.mp3",+ "demo/audio/wrap.mp3",+ "vendor/newbie-panel.js",+ "dockbar.json",+ "install.sh",+ "uninstall.sh",+ "package.json"+ ],+ "scripts": {+ "install": "./install.sh",+ "uninstall": "./uninstall.sh"+ },+ "dependencies": {+ "adom/adom-bridge": "^2.0.3"+ },+ "sample_prompts": [+ {+ "label": "Open in my browser",+ "prompt": "open the wiki in my real chrome work profile"+ },+ {+ "label": "Log in for me",+ "prompt": "log into digikey for me using my saved password"+ },+ {+ "label": "Form behind a login",+ "prompt": "fill out this form on the site I'm already signed into"+ },+ {+ "label": "Download behind auth",+ "prompt": "download that datasheet, it's behind a login"+ },+ {+ "label": "Install the extension",+ "prompt": "install the adom native browser extension in my chrome"+ }+ ],+ "org": "adom",+ "author": {+ "name": "John Lauer",+ "email": "[email protected]"+ }+}
vendor/newbie-panel.js+154−7@@ -139,6 +139,35 @@ return safe.replace(re, '<b>$1</b>'); } + /* ---- copy, with a fallback that does not need the async clipboard ------+ * navigator.clipboard.writeText needs a secure context AND, in several embedded webviews,+ * transient user activation plus a clipboard-write permission that is simply absent. In+ * ah's webview it rejects with NotAllowedError, so the ONLY path the panel had would fail+ * and the user got "Could not reach the clipboard" on a button that looks like it works.+ * The execCommand path is deprecated but synchronous, needs no permission, and works in+ * exactly the contexts the async API refuses. Try the modern one, fall back, and only then+ * report failure. */+ function copyText(text) {+ return new Promise(function (resolve, reject) {+ function legacy() {+ try {+ var ta = document.createElement('textarea');+ ta.value = text;+ ta.setAttribute('readonly', '');+ ta.style.cssText = 'position:fixed;top:0;left:-9999px;opacity:0';+ document.body.appendChild(ta);+ ta.select(); ta.setSelectionRange(0, ta.value.length);+ var ok = document.execCommand && document.execCommand('copy');+ ta.remove();+ ok ? resolve('legacy') : reject(new Error('execCommand refused'));+ } catch (e) { reject(e); }+ }+ if (navigator.clipboard && navigator.clipboard.writeText) {+ navigator.clipboard.writeText(text).then(function () { resolve('async'); }, legacy);+ } else legacy();+ });+ }+ /* ---- toasts ---------------------------------------------------------- */ var toastHost = null; function toast(msg, kind, ms) {@@ -275,7 +304,7 @@ tipFor(cp, 'Copy this phrase'); cp.addEventListener('click', function (e) { e.stopPropagation();- (navigator.clipboard ? navigator.clipboard.writeText(phrase) : Promise.reject())+ copyText(phrase) .then(function () { toast('Copied. Paste it to your AI.', 'ok'); }) .catch(function () { toast('Could not reach the clipboard. Select the phrase and copy it.', 'err'); }); });@@ -383,6 +412,18 @@ function selfCheck(panel) { panel = panel || document.querySelector('.anp'); if (!panel) return { ok: false, error: 'no .anp panel on the page' };+ /* REFUSE TO REPORT FROM A COLLAPSED LAYOUT. An unforegrounded tab returns zero-height+ * rects while computed styles still resolve normally, so selfCheck produced EIGHT GREEN+ * ROWS beside two red ones: "panel gutter: 0px left, 744px right" and "columns: want 1,+ * got 3". That mixed shape is far more dangerous than an all-zero one, because it reads+ * as a specific layout regression rather than an invalid run, and project-manager nearly+ * filed it as a bug. A refusal cannot be misread. (project-manager, 2026-08-25.) */+ var w0 = Math.round(panel.getBoundingClientRect().width);+ if (w0 === 0) return { ok: false, invalid: true,+ error: 'panel measures 0px wide: this tab is not foregrounded, so every layout row would ' ++ 'be computed from a collapsed pane while the style rows pass normally. Activate ' ++ 'the tab by name and re-run. No rows reported, because a partial result from an ' ++ 'unrendered pane looks like a real bug.' }; var cs = getComputedStyle(panel), rows = [], ok = true; /* Chrome reports the USED border-width, snapped to whole device pixels, so a declared * 1px reads as 0.666667px at dpr 1.5. Compare against the snapped value, not the@@ -427,12 +468,88 @@ * column while the other three rendered TWO, at an identical pane width. The panel was * conformant in all four; the PAGE around it was not. The fleet gutter is 16px a side, * so the panel's border box is the pane minus 32. */- var vw = document.documentElement.clientWidth;- var want32 = vw - 32, gotW = Math.round(panel.getBoundingClientRect().width);- var gutterOk = Math.abs(gotW - want32) <= 2;+ /* Measure against the panel's OWN CONTAINING BLOCK, never documentElement.+ * The first version compared to documentElement.clientWidth, which silently assumes the+ * PAGE scrolls. An app-shell dashboard (position:fixed shell, 44px header, scroller on an+ * inner .app-body) has no document scrollbar, so clientWidth is the full pane and the+ * inner scrollbar is charged to content instead: a correctly built app-shell page fails+ * by exactly one scrollbar width, and only pages that happen to scroll at document level+ * pass. Since app-shell is the prescribed structure, the check was failing the apps that+ * followed the skill. The containing block is scrollbar-agnostic and still catches the+ * real defect, which is a page adding its own padding around the mount point.+ * (parts-search, 2026-08-25, from reading the implementation rather than the prose.) */+ /* Measure BOTH INSETS AGAINST THE PANE, and never against a named element.+ * Two earlier versions asked the wrong element how wide the world is. documentElement+ * assumes the page scrolls, and fails every app-shell dashboard by one scrollbar width.+ * The mount div assumes the 16px lives on the mount div, and reds a visually perfect page+ * that puts the gutter on a shared outer container instead. Both encode a STRUCTURE the+ * fleet was never promised. What the contract actually says is where the panel's EDGES+ * land, so measure exactly that: find the nearest scrolling ancestor (or the document),+ * and check the panel sits 16px inside its client box on both sides. Structure-agnostic:+ * it does not care which element carries the padding, or how many wrappers sit between.+ * (pup found the mount-div version would red an outer-container page, before six apps+ * adopted the pattern I had recommended; parts-search found the documentElement one.) */+ var sc = panel.parentElement;+ while (sc && sc !== document.body) {+ /* A DECLARED scroller is the pane whether or not it is scrolling RIGHT NOW. Requiring+ * it to overflow was a bug with a nastier shape than the two before it: with+ * scrollbar-gutter:stable (which this pack mandates, so results arriving cause no+ * reflow) the track is reserved even when nothing overflows, so the loop walked PAST+ * the real pane, fell back to documentElement, and reported the right inset over by+ * one scrollbar width. The SAME PAGE passed when populated and failed when empty, and+ * empty is a dashboard's first-load state. Predicted from a code read by parts-search+ * and confirmed here: 16 left / 31 right, exact. */+ var so = getComputedStyle(sc).overflowY;+ if (so === 'auto' || so === 'scroll') break;+ sc = sc.parentElement;+ }+ var paneLeft, paneW;+ if (sc && sc !== document.body) { paneLeft = sc.getBoundingClientRect().left; paneW = sc.clientWidth; }+ else { paneLeft = 0; paneW = document.documentElement.clientWidth; }+ var pr = panel.getBoundingClientRect();+ var insetL = Math.round(pr.left - paneLeft);+ var insetR = Math.round(paneLeft + paneW - pr.right);+ var gutterOk = Math.abs(insetL - 16) <= 2 && Math.abs(insetR - 16) <= 2; if (!gutterOk) ok = false;- rows.push({ what: 'panel gutter', want: want32 + 'px wide (16px a side)',- got: gotW + 'px (' + Math.round((vw - gotW) / 2) + 'px a side)', pass: gutterOk });+ rows.push({ what: 'panel gutter', want: '16px inset both sides',+ got: insetL + 'px left, ' + insetR + 'px right', pass: gutterOk });+ /* THE LEFT-EDGE ROW closes the hole the gutter row cannot see. The gutter row measures+ * against whatever element the app mounts INTO, so an app that mounts into a wrapper+ * which itself carries padding passes while sitting inset from its neighbours, and the+ * fleet mismatch comes back in a form the check is blind to. This asserts the panel's+ * left edge against the PANE, which covers the whole ancestor chain in one number.+ * (parts-search, 2026-08-25, reasoning about what the new yardstick still could not see.) */+ /* PHRASE HEADROOM AS A ROW, not a console warning nobody reads: "does not truncate TODAY"+ * and "has room" are different claims.+ * MEASURE need AGAINST AN OFFSCREEN nowrap PROBE. Do NOT use clientWidth - scrollWidth:+ * for a block span with overflow:hidden, scrollWidth is max(clientWidth, content), so that+ * expression SATURATES AT 0 for anything that fits, whether it fits by 1px or by 180px. It+ * still goes negative correctly when text overflows, so it looks like a working metric.+ * Two apps independently reported "0px headroom, one character from truncating" off it;+ * remeasured with this probe, neither had less than 141px of slack. A metric that can only+ * return one value on success is not evidence of success. */+ var spans = [].slice.call(panel.querySelectorAll('.anp-phrase span'));+ if (spans.length) {+ var probe = document.createElement('span');+ probe.style.cssText = 'position:absolute;visibility:hidden;white-space:nowrap;font:' ++ getComputedStyle(spans[0]).font;+ document.body.appendChild(probe);+ var worst = 0, worstTxt = '', tight = 0;+ spans.forEach(function (sp) {+ probe.textContent = sp.textContent;+ var need = Math.ceil(probe.getBoundingClientRect().width);+ var have = Math.round(sp.getBoundingClientRect().width);+ var slack = have - need;+ if (worstTxt === '' || slack < worst) { worst = slack; worstTxt = sp.textContent.trim().slice(0, 40); }+ if (slack < 8) tight++;+ });+ probe.remove();+ var headOk = worst >= 8;+ if (!headOk) ok = false;+ rows.push({ what: 'phrase headroom', want: 'at least 8px of slack on every phrase',+ got: worst + 'px on ' + JSON.stringify(worstTxt) + (tight > 1 ? ' (' + tight + ' tight)' : ''),+ pass: headOk });+ } var fam = getComputedStyle(panel.querySelector('.anp-phrase span') || panel).fontFamily; var famOk = /Satoshi/.test(fam); if (!famOk) ok = false;@@ -440,6 +557,36 @@ return { ok: ok, rows: rows }; } - root.AdomNewbiePanel = { mount: mount, toast: toast, selfCheck: selfCheck, version: '2.3.0',+ /* checkSend(apiBase) - prove the send path EXISTS, without sending anything.+ * gate() fails OPEN on purpose, so a broken probe cannot block a user from sending. The+ * consequence, which kicad found in itself: A MISSING GATE READS AS AN ALLOWED GATE. Its+ * /updater/status route had been lost in a rebuild, the gate got HTML, caught, and returned+ * open, so the version gate had been silently no-opping. Nothing anywhere said so.+ * This makes that visible. It never posts a prompt: the run-prompt route is probed with+ * OPTIONS, so it can report "wired" without creating a thread in anyone's list. */+ function checkSend(base) {+ base = base == null ? '' : base;+ var rows = [], ok = true;+ function add(what, want, got, pass) { rows.push({what: what, want: want, got: got, pass: pass}); if (!pass) ok = false; }+ return fetch(base + '/updater/status', {cache: 'no-store'})+ .then(function (r) { return r.text().then(function (t) { return {s: r.status, t: t}; }); })+ .then(function (res) {+ var v = null, parsed = false;+ try { var j = JSON.parse(res.t); parsed = true; v = j && j.updater && j.updater.current_version; } catch (e) {}+ add('gate route wired', '200 with JSON', res.s + (parsed ? ' JSON' : ' NOT JSON'), res.s === 200 && parsed);+ add('gate reports a version', 'updater.current_version', String(v), !!v);+ if (v) add('version meets the minimum', MIN_API.join('.'), String(v), verGte(verNum(v), MIN_API));+ })+ .catch(function (e) { add('gate route wired', '200 with JSON', 'FETCH FAILED: ' + e.message, false); })+ .then(function () {+ return fetch(base + '/ai-threads/run-prompt', {method: 'OPTIONS'})+ .then(function (r) { add('send route wired', 'not 404', String(r.status), r.status !== 404); })+ .catch(function (e) { add('send route wired', 'not 404', 'FETCH FAILED: ' + e.message, false); });+ })+ .then(function () { return {ok: ok, rows: rows}; });+ }++ root.AdomNewbiePanel = { mount: mount, toast: toast, selfCheck: selfCheck, copyText: copyText,+ checkSend: checkSend, version: '2.19.1', TWO_COL_MIN: TWO_COL_MIN, PHRASE_BUDGET: PHRASE_BUDGET }; })(typeof window !== 'undefined' ? window : this);