app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
← Commit history
Annotate toolbar polish from John's live test, per the Adom UI design skills. The toolbar is draggable by a visible six-dot grip and clamps inside the viewport (floating-windows rules). Every control now has a real tooltip through a proper tooltip system: body-level fixed element, 600ms reveal, 0.18s fade and translate, measured after content is set, clamped to the viewport on both axes and flipping above rather than clipping (the tooltips skill contract), with carefully written copy per tool. Auto capture is on by default so the common single-circle flow needs zero extra clicks. And the view toggle is no longer out of sync: /health now carries the CHECKED wiki auth state per session, the extension popup labels the toggle from that ground truth instead of a cached belief, and hides the toggle entirely on non-wiki pages
5 files changed
+88−31
adom-bridge-puppeteer-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "2.0.55",- "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.55/adom-bridge-puppeteer-v2.0.55.zip",- "sha256": "e45c2bf05532f887c898be30be3457c62cba09c99483e80c3c737583cdeb40e6",- "size": 744050,+ "version": "2.0.56",+ "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.56/adom-bridge-puppeteer-v2.0.56.zip",+ "sha256": "84543102de7229b2bfe08868902228d659266589fe13a99e40feac8680b09646",+ "size": 745602, "verbPrefixes": [ "browser_", "desktop_recorder_",
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-2.0.55+2.0.56
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "2.0.55",+ "version": "2.0.56", "description": "Drives a browser ALREADY on the machine \u2014 installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) \u2014 launched with a FRESH isolated profile, so the common case has NO ~150 MB Chrome-for-Testing download and it never touches the user's real tabs/logins. Binds its local server to LOOPBACK (127.0.0.1) only, so no Windows Firewall 'allow Node through the firewall' prompt ever pops. Chrome for Testing is fetched only as a LAST RESORT if the machine has no Chromium browser at all. SPAWN-VERIFIED fallback: pup tries each detected browser (Chrome \u2192 Edge \u2192 CfT) and actually launches it, falling through to the next if one won't start (and handling browsers like Edge that relaunch onto another PID), so a corrupt/partial install never hard-fails an open. The browser that launches is cached as the default (persisted across restarts) so it isn't re-probed each time. On spawn the bridge SELF-WARMS: detects browsers + disk, and only if the box has no Chrome/Edge and no cached CfT does it background-fetch CfT so the first open is instant. pup_use pins a specific browser (chrome | edge | cft | auto); {browser:'chrome',install:true} downloads & installs REAL Google Chrome (user-scoped, no admin) on an Edge-only box. pup_readiness (READ-ONLY) reports which browser will be used + the full candidate list + the default + free disk. Every failure returns a specific errorCode + _hint (chrome_install_no_disk, chrome_download_network, chrome_install_permission, chrome_stable_not_detected, \u2026). Launches with FULL browser permissions by default for full debug (strictPermissions:true opts out). Headful automation: open/close windows+tabs, navigate, screenshot, eval JS, record. pup is a GENERIC, non-profile browser only \u2014 every session is a fresh isolated Chrome/Edge process, never the user's real signed-in browser; profile defaults to sessionId. To drive the user's REAL Chrome/Edge (their profile, cookies, SSO), use the Adom browser EXTENSION's nbrowser_* verbs \u2014 a separate bridge, not pup's job. macOS (v1.9.74+): full parity with mac-native mechanics \u2014 background-by-default is the app-level hide (windows stay drivable/screenshottable while hidden), raise/lower map to unhide+activate/hide, pup_alert_window posts a native toast whose click focuses the window, pup_use {browser:'chrome',install:true} installs Chrome from the official dmg, and taskbar identity/badging remains Windows-only.", "homepage": "https://wiki.adom.inc/adom/adom-bridge", "author": "Adom Inc.",
src/extension/popup.js+12−9@@ -47,16 +47,19 @@ async function cmd(port, command, args) { try { const sessions = (health.sessions || []); const hit = sessions.find(s => s.url && tab && tab.url && (s.url === tab.url || tab.url.startsWith((s.url || '').split('?')[0])));- if (hit) sid = hit.sessionId;- if (sid) {- const d = await cmd(port, 'pup_list_windows', {});- const rows = JSON.parse(d.output || '{}').sessions || d.sessions || [];- const row = rows.find(r2 => r2.sessionId === sid) || (Array.isArray(rows) ? rows.find(r2 => r2.sessionId === sid) : null);- if (row) { owner = row.owner; view = row.wikiView || row.view || null; }- }+ if (hit) { sid = hit.sessionId; owner = hit.owner || null; } } catch (e) {} ownerEl.innerHTML = owner ? 'owned by <b>' + owner.replace(/</g, '<') + '</b>' : (sid ? sid : 'not a pup window');- if (view === 'public') document.getElementById('viewLabel').textContent = 'Switch to logged-in view';+ // View toggle: label from the CHECKED auth state (pup verifies against the page, /health carries+ // it), never from a cached belief. Non-wiki pages hide the toggle entirely.+ const isWiki = !!(tab && tab.url && /https:\/\/wiki\.adom\.inc\//.test(tab.url));+ const hit2 = (health.sessions || []).find(s2 => s2.sessionId === sid);+ const authed = hit2 ? hit2.authed : null;+ const viewBtn = document.querySelector('button[data-act="view"]');+ if (!isWiki) { viewBtn.style.display = 'none'; }+ else if (authed === true) { view = 'authed'; document.getElementById('viewLabel').textContent = 'Switch to public view'; }+ else if (authed === false) { view = 'public'; document.getElementById('viewLabel').textContent = 'Switch to logged-in view'; }+ else { view = null; document.getElementById('viewLabel').textContent = 'Toggle logged-in/public view'; } foot.innerHTML = '<span class="dot">โ</span> pup ' + (health.version || '') + ' ยท port ' + port; menu.addEventListener('click', async (e) => {@@ -66,7 +69,7 @@ async function cmd(port, command, args) { try { if (act === 'annotate') await cmd(port, 'pup_annotate', { sessionId: sid }); else if (act === 'snip') await cmd(port, 'pup_annotate', { sessionId: sid, tool: 'snip' });- else if (act === 'view') await cmd(port, 'pup_wiki_set_view', { sessionId: sid, view: view === 'public' ? 'authed' : 'public' });+ else if (act === 'view') await cmd(port, 'pup_wiki_set_view', { sessionId: sid, view: view === 'authed' ? 'public' : 'authed' }); else if (act === 'dashboard') await cmd(port, 'pup_dashboard', {}); else if (act === 'info') await cmd(port, 'pup_window_info', { sessionId: sid }); else if (act === 'close') { if (confirm('Close ' + (owner ? owner + "'s" : 'this') + ' pup window?')) await cmd(port, 'pup_close_window', { sessionId: sid }); }
src/server.js+70−16@@ -5284,7 +5284,7 @@ function appIconReprimand(url, hadFavicon) { const ANNOTATE_OVERLAY_JS = `(() => { if (window.__pupAnnotate) { window.__pupAnnotate.show(); return 'shown'; } var initTool = window.__pupAnnotateTool || 'pen'; window.__pupAnnotateTool = null;- var S = { tool: 'pen', color: '#f85149', strokes: [], cur: null, quick: initTool === 'snip', seq: 0 };+ var S = { tool: 'pen', color: '#f85149', strokes: [], cur: null, quick: true, seq: 0 }; var COLORS = ['#f85149', '#d29922', '#3fb950', '#64abff', '#8c6bf7', '#e6edf3']; var wrap = document.createElement('div'); wrap.id = '__pup_annotate';@@ -5295,6 +5295,58 @@ const ANNOTATE_OVERLAY_JS = `(() => { var bar = document.createElement('div'); bar.style.cssText = 'position:fixed;top:14px;left:50%;transform:translateX(-50%);display:flex;gap:6px;align-items:center;background:#161b22f2;border:1px solid #30363d;border-radius:999px;padding:7px 12px;pointer-events:auto;font:13px Satoshi,system-ui,sans-serif;color:#c9d1d9;box-shadow:0 6px 24px #000a;'; wrap.appendChild(bar);+ // drag grip (floating-windows: draggable by a visible grip, clamped inside the viewport)+ var grip = document.createElement('div');+ grip.title = '';+ grip.style.cssText = 'width:14px;height:22px;cursor:grab;display:grid;grid-template-columns:1fr 1fr;gap:2px;align-content:center;justify-items:center;margin-right:2px;';+ for (var gi = 0; gi < 6; gi++) { var gd = document.createElement('div'); gd.style.cssText = 'width:3px;height:3px;border-radius:50%;background:#8b949e;'; grip.appendChild(gd); }+ bar.appendChild(grip);+ (function () {+ var drag = null;+ grip.addEventListener('pointerdown', function (e) {+ var r = bar.getBoundingClientRect();+ if (bar.style.transform) { bar.style.transform = ''; bar.style.left = r.left + 'px'; bar.style.top = r.top + 'px'; }+ drag = { dx: e.clientX - r.left, dy: e.clientY - r.top };+ grip.setPointerCapture(e.pointerId); grip.style.cursor = 'grabbing'; e.preventDefault();+ });+ grip.addEventListener('pointermove', function (e) {+ if (!drag) return;+ var w = bar.offsetWidth, h = bar.offsetHeight, M = 8;+ var x = Math.min(Math.max(M, e.clientX - drag.dx), innerWidth - w - M);+ var y = Math.min(Math.max(M, e.clientY - drag.dy), innerHeight - h - M);+ bar.style.left = x + 'px'; bar.style.top = y + 'px';+ });+ grip.addEventListener('pointerup', function () { drag = null; grip.style.cursor = 'grab'; });+ })();+ // tooltip singleton (tooltips skill: fixed, body-level z-max, 600ms reveal, 0.18s ease+ // fade+translate, measured AFTER content set, clamped to the viewport on BOTH axes, flips+ // above when it would clip the bottom)+ var tipEl = document.createElement('div');+ tipEl.style.cssText = 'position:fixed;z-index:2147483647;max-width:280px;background:#21262d;border:1px solid #30363d;border-radius:8px;padding:7px 10px;font:12px Satoshi,system-ui,sans-serif;color:#c9d1d9;box-shadow:0 6px 20px #000b;pointer-events:none;opacity:0;transform:translateY(4px);transition:opacity 0.18s ease,transform 0.18s ease;text-transform:none;';+ wrap.appendChild(tipEl);+ var tipTimer = null;+ function tipShow(anchor, text) {+ clearTimeout(tipTimer);+ tipTimer = setTimeout(function () {+ tipEl.textContent = text;+ tipEl.style.opacity = '0';+ // measure only after content is set and laid out+ requestAnimationFrame(function () {+ var a = anchor.getBoundingClientRect(), M = 8;+ var w = tipEl.offsetWidth, h = tipEl.offsetHeight;+ var x = a.left + a.width / 2 - w / 2;+ var y = a.bottom + 10;+ if (y + h > innerHeight - M) y = a.top - h - 10; // flip above instead of clipping+ x = Math.min(Math.max(M, x), innerWidth - w - M);+ y = Math.min(Math.max(M, y), innerHeight - h - M);+ tipEl.style.left = x + 'px'; tipEl.style.top = y + 'px';+ tipEl.style.opacity = '1'; tipEl.style.transform = 'translateY(0)';+ });+ }, 600);+ }+ function tipHide() { clearTimeout(tipTimer); tipEl.style.opacity = '0'; tipEl.style.transform = 'translateY(4px)'; }+ function tipFor(el, text) { el.addEventListener('mouseenter', function () { tipShow(el, text); }); el.addEventListener('mouseleave', tipHide); el.addEventListener('pointerdown', tipHide); el.removeAttribute('title'); }+ tipFor(grip, 'Drag to move this toolbar anywhere it is not in your way.'); var ctx = cv.getContext('2d'); // โโ Photoshop-style tool cursors: SVG data-URI, hotspot at the working tip, pen carries a // live swatch of the current color (rebuilt on color change).@@ -5398,28 +5450,30 @@ const ANNOTATE_OVERLAY_JS = `(() => { b.style.cssText = 'background:none;border:1px solid transparent;border-radius:8px;color:#c9d1d9;padding:3px 8px;cursor:pointer;font:inherit;display:flex;align-items:center;gap:5px;'; b.onmouseenter = function () { b.style.background = '#1c2128'; }; b.onmouseleave = function () { b.style.background = S.tool === b.dataset.tool ? 'rgba(0,184,177,.13)' : 'none'; };- b.onclick = fn; if (id) b.dataset.tool = id; bar.appendChild(b); return b;+ b.onclick = fn; if (id) b.dataset.tool = id; bar.appendChild(b);+ if (title) { tipFor(b, title); }+ return b; } function selTool(id) { S.tool = id; setCursor(); [].forEach.call(bar.querySelectorAll('button[data-tool]'), function (x) { x.style.background = x.dataset.tool === id ? 'rgba(0,184,177,.13)' : 'none'; x.style.borderColor = x.dataset.tool === id ? 'rgba(0,184,177,.45)' : 'transparent'; }); }- btn('Pen', 'freehand', function () { selTool('pen'); }, 'pen', 'pen');- btn('Arrow', 'arrow', function () { selTool('arrow'); }, 'arrow', 'arrow');- btn('Box', 'rectangle', function () { selTool('box'); }, 'box', 'box');- btn('Text', 'text note', function () { selTool('text'); }, 'text', 'text');- btn('Snip', 'marquee a region: captures it the moment you release', function () { selTool('snip'); }, 'snip', 'snip');+ btn('Pen', 'Draw freehand. With Auto on, the capture fires the moment you release the mouse: cropped tight to your drawing, copied to your clipboard.', function () { selTool('pen'); }, 'pen', 'pen');+ btn('Arrow', 'Point at the thing you mean. Drag from tail to tip; the arrowhead lands where you release.', function () { selTool('arrow'); }, 'arrow', 'arrow');+ btn('Box', 'Frame a region with a rectangle. Drag corner to corner.', function () { selTool('box'); }, 'box', 'box');+ btn('Text', 'Drop a short text note onto the page. Click where the note should start.', function () { selTool('text'); }, 'text', 'text');+ btn('Snip', 'Capture exactly a region, like Win+Shift+S: drag a marquee and it is copied to your clipboard the moment you release.', function () { selTool('snip'); }, 'snip', 'snip'); var dot = document.createElement('button');- dot.title = 'drawing color'; dot.style.cssText = 'width:20px;height:20px;border-radius:50%;border:2px solid #8b949e;cursor:pointer;background:' + S.color + ';';+ dot.removeAttribute && dot.removeAttribute('title'); dot.style.cssText = 'width:20px;height:20px;border-radius:50%;border:2px solid #8b949e;cursor:pointer;background:' + S.color + ';'; dot.onclick = function () { var i = (COLORS.indexOf(S.color) + 1) % COLORS.length; S.color = COLORS[i]; dot.style.background = S.color; setCursor(); };- bar.appendChild(dot);+ bar.appendChild(dot); tipFor(dot, 'Drawing color. Click to cycle: red, yellow, green, blue, purple, white. The pen cursor shows the current color.'); var qk = document.createElement('button'); function qkPaint() { qk.style.cssText = 'background:' + (S.quick ? 'rgba(0,184,177,.13)' : 'none') + ';border:1px solid ' + (S.quick ? 'rgba(0,184,177,.55)' : 'transparent') + ';border-radius:8px;color:' + (S.quick ? '#00d4cb' : '#c9d1d9') + ';padding:3px 8px;cursor:pointer;font:inherit;display:flex;align-items:center;gap:5px;'; }- qk.appendChild(icon(ICONS.auto[0])); qk.appendChild(document.createTextNode('Auto')); qk.title = 'Quick capture: copy to clipboard the moment you lift the mouse';+ qk.appendChild(icon(ICONS.auto[0])); qk.appendChild(document.createTextNode('Auto')); qk.onclick = function () { S.quick = !S.quick; qkPaint(); };- qkPaint(); bar.appendChild(qk);- btn('Undo', 'undo last', function () { S.strokes.pop(); redraw(); }, null, 'undo');- btn('Clear', 'clear all', function () { S.strokes = []; redraw(); }, null, 'clear');- var done = btn('Done', 'capture for the owning AI thread', function () { emit(null, true); }, null, 'done');+ qkPaint(); bar.appendChild(qk); tipFor(qk, 'Auto capture (on by default): every time you lift the mouse, the marked-up area is captured and copied to your clipboard, ready to paste into any AI chat. Turn off to compose multi-step drawings and capture once with Done.');+ btn('Undo', 'Remove the last thing you drew. Click again to keep unwinding.', function () { S.strokes.pop(); redraw(); }, null, 'undo');+ btn('Clear', 'Erase every drawing on this page. The page itself is untouched.', function () { S.strokes = []; redraw(); }, null, 'clear');+ var done = btn('Done', 'Capture the annotated area (tight-cropped around your drawing), copy it to your clipboard, and leave the drawing visible so the AI thread that owns this window sees it too.', function () { emit(null, true); }, null, 'done'); done.style.cssText += 'background:rgba(0,184,177,.13);border:1px solid rgba(0,184,177,.55);color:#00d4cb;font-weight:600;';- btn('', 'cancel (remove drawing)', function () { wrap.remove(); window.__pupAnnotate = null; window.__pupAnnotateResult = { done: false, ts: Date.now() }; }, null, 'cancel');+ btn('', 'Close the annotation toolbar and remove all drawings from the page.', function () { wrap.remove(); window.__pupAnnotate = null; window.__pupAnnotateResult = { done: false, ts: Date.now() }; }, null, 'cancel'); window.__pupAnnotate = { show: function () { wrap.style.display = ''; bar.style.display = 'flex'; cv.style.pointerEvents = 'auto'; } }; addEventListener('resize', fit); document.documentElement.appendChild(wrap);@@ -10635,7 +10689,7 @@ const server = http.createServer(async (req, res) => { try { pageUrl = s.page.url(); } catch (e) {} // v2.0.51: owner + view ride /health so the toolbar extension can badge tabs per owning // thread and label its view toggle without a heavier verb round-trip.- sessionList.push({ sessionId: id, url: pageUrl, active: id === activeSessionId, owner: s.owner || s._ownerThread || null, view: s._wikiView || null });+ sessionList.push({ sessionId: id, url: pageUrl, active: id === activeSessionId, owner: s.owner || s._ownerThread || null, view: s._wikiView || null, authed: (s._wikiAuthed === true ? true : (s._wikiAuthed === false ? false : null)) }); } sendJSON(res, { status: 'ok',