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
Add Close-ALL-pup-windows jump-list task to every window, unregister AUMIDs on close-all, and reconnect+reattach disconnected-but-alive sessions via rescanProfile so a zombie window stops showing the old default icon
4 files changed
+77−27
adom-bridge-puppeteer-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "1.9.50",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.50/adom-bridge-puppeteer-v1.9.50.zip",- "sha256": "b487261f476d8d3dd5bc4faac2c11d8ebac17461d5d54cedd21900bf2e876cc6",- "size": 298485,+ "version": "1.9.51",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.51/adom-bridge-puppeteer-v1.9.51.zip",+ "sha256": "216796fc4447779b85db47f69c6d315c692544754cb28304ca7a2a276c717465",+ "size": 299556, "verbPrefixes": [ "browser_", "desktop_recorder_",
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.50+1.9.51
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "1.9.50",+ "version": "1.9.51", "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. browser_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. browser_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.", "homepage": "https://wiki.adom.inc/adom/adom-desktop", "author": "Adom Inc.",
src/server.js+71−21@@ -2067,29 +2067,38 @@ async function updateWikiJumplist(session, sessionId) { const appId = (session && session._curAppId) || `${PUP_APP_ID}.${sessionId}`; let url = null; try { url = (session.page && session.page.url()) || null; } catch (e) {} const view = isAdomUrl(url) ? (session._wikiView === 'authed' ? 'authed' : 'public') : null;+ // v1.9.51: the jump-list STATE is now (view + a constant close-all task). The close-all task is+ // on EVERY pup window, so the idempotency key is just the view — a menu that already reflects this+ // view already has the close-all row too. if (session._wikiJumplistView === view) return; // menu already reflects this state- // v1.9.5: the title MUST name the Adom wiki (John: a bare "Switch to logged-in view" reads- // generic, as if it'd work on any site). Lead with "Adom wiki" so the menu is self-describing.- // v1.9.6: give the task the Adom Pup ICON (iconPath) — without it Windows shows a generic+ // v1.9.6: give each task the Adom Pup ICON (iconPath) — without it Windows shows a generic // blank-document glyph (John: "why is your icon generic for that menu item?"). const _taskIcon = pupIconPath();- // v1.9.9: WINDOWS ARGV QUOTING — THE reason clicking the task did NOTHING. The jump-list task- // line is parsed by the CLI's argv parser, which STRIPS unescaped double quotes. Raw JSON- // ({"sessionId":"x","view":"authed"}) therefore arrived as {sessionId:x,view:authed} and the CLI- // died instantly with `Invalid JSON args: key must be a string at line 1 column 2` (exit 1) —- // a 50ms cmd flash and no switch, exactly what John reported. Verified both ways on his box.- // Fix: wrap the JSON in quotes with the inner quotes BACKSLASH-escaped, so argv delivers it- // intact as ONE argument. Any future jump-list task passing JSON MUST use this escaping.- const taskArgs = (v) => `browser_wiki_set_view "${JSON.stringify({ sessionId, view: v }).replace(/"/g, '\\"')}"`;+ // v1.9.9: WINDOWS ARGV QUOTING — THE reason clicking a task did NOTHING. The jump-list task line is+ // parsed by the CLI's argv parser, which STRIPS unescaped double quotes, so raw JSON arrived+ // mangled and the CLI died with `Invalid JSON args`. Wrap JSON in quotes with the inner quotes+ // BACKSLASH-escaped so argv delivers it intact as ONE argument. Any JSON-carrying task MUST use this.+ const jarg = (verb, obj) => `${verb} "${JSON.stringify(obj).replace(/"/g, '\\"')}"`;+ const taskArgs = (v) => jarg('browser_wiki_set_view', { sessionId, view: v });+ // v1.9.51: a CLOSE-ALL task on every pup window (John: "i have like 8 open right now"). It calls+ // browser_close with no sessionId, which kills every pup Chrome + clears every session. The empty+ // "{}" still goes through the same escaped-argv path so the CLI parses it as one JSON argument.+ const closeAllTask = {+ title: 'Close ALL Adom Pup windows',+ description: 'Close every Adom Pup browser window at once (all sessions, every tab)',+ iconPath: _taskIcon,+ args: jarg('browser_close', {}),+ };+ // v1.9.5: a wiki window also gets a view-toggle task; lead with "Adom wiki" so it self-describes. let tasks = []; if (view === 'authed') {- tasks = [{ title: 'Adom wiki: switch to public view', description: 'Reload this Adom wiki page logged OUT — exactly what the public/world sees', iconPath: _taskIcon, args: taskArgs('public') }];+ tasks.push({ title: 'Adom wiki: switch to public view', description: 'Reload this Adom wiki page logged OUT — exactly what the public/world sees', iconPath: _taskIcon, args: taskArgs('public') }); } else if (view === 'public') {- tasks = [{ title: 'Adom wiki: switch to logged-in view', description: 'Reload this Adom wiki page SIGNED IN — private source, drafts, owner-only cards', iconPath: _taskIcon, args: taskArgs('authed') }];+ tasks.push({ title: 'Adom wiki: switch to logged-in view', description: 'Reload this Adom wiki page SIGNED IN — private source, drafts, owner-only cards', iconPath: _taskIcon, args: taskArgs('authed') }); }- // The shell ignores a jump list on an unregistered AUMID — ensure it's registered first- // (idempotent; only registers when there IS a menu to show).- if (tasks.length && !_registeredPupAumids.has(appId)) {+ tasks.push(closeAllTask); // always present, listed after any window-specific task+ // The shell ignores a jump list on an unregistered AUMID — ensure it's registered first.+ if (!_registeredPupAumids.has(appId)) { await registerPupAumid(appId, sessionId); if (!_registeredPupAumids.has(appId)) return; }@@ -2665,9 +2674,28 @@ async function adoptDraggedOutTabs(sessionId) { // band — never inside a targetcreated handler, to keep the pipe safe). async function reclaimUnmanagedWindows() { let reclaimed = 0;+ const handledByRescan = new Set(); try {- // Part B first: pup profiles whose Chrome is alive but that pup is not connected to at all.- // Without this an orphan is invisible — it has no browser entry, so Part A cannot see its pages.+ // v1.9.51 — Part 0: a session whose CDP DISCONNECTED but whose Chrome window is still alive.+ // handleBrowserDisconnect deletes the browser entry and marks the session `_lostBrowser` with+ // ZERO tabs; nothing re-evaluated it, so it lingered as a zombie stamped ".plain" and fell back+ // to the OLD default pup icon (exactly the stale icon John flagged on the shotlog window). If its+ // Chrome is truly gone, drop the session AND unregister its AUMID so no stale taskbar identity is+ // left; if its Chrome is alive, Part B below reconnects and rescanProfile reattaches it by title.+ for (const [sid, s] of [...sessions]) {+ if (!s._lostBrowser || (s.tabs || []).length) continue;+ let port = null;+ try { port = parseInt(fs.readFileSync(path.join(PROFILES_DIR, s.profileName, 'DevToolsActivePort'), 'utf8').trim().split('\n')[0], 10) || null; } catch (e) {}+ const alive = port && await fetch(`http://127.0.0.1:${port}/json/version`).then(r => r && r.ok).catch(() => false);+ if (alive) continue; // Part B will reconnect + rescanProfile reattaches this same session+ console.log(`[reclaim] zombie session "${sid}" (_lostBrowser, 0 tabs, Chrome gone) — dropping it and its taskbar identity`);+ try { if (s._curAppId && s._curAppId !== PUP_APP_ID) await unregisterPupAumid(s._curAppId); } catch (e) {}+ sessions.delete(sid);+ try { deleteSessionFile(sid); } catch (e) {}+ }++ // Part B: pup profiles whose Chrome is alive but that pup is not connected to. Covers a fully+ // orphaned profile AND a session whose CDP dropped (browser entry deleted on disconnect). let profileDirs = []; try { profileDirs = fs.readdirSync(PROFILES_DIR, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name); } catch (e) {} for (const profileName of profileDirs) {@@ -2683,12 +2711,27 @@ async function reclaimUnmanagedWindows() { browsers.set(profileName, { browser, refCount: 0 }); wirePopupTracking(browser, profileName); browser.on('disconnected', () => handleBrowserDisconnect(browser, profileName));- console.log(`[reclaim] re-connected to ORPHANED profile "${profileName}" on port ${port} (Chrome outlived its session)`);+ console.log(`[reclaim] re-connected to profile "${profileName}" on port ${port} (Chrome outlived its CDP link)`);+ // v1.9.51: reattach by TITLE TAG. rescanProfile reuses the EXISTING "(session: <id>)" session+ // instead of minting a new "<profile>-winXXXX" (which would leave the tagged session a zombie+ // AND duplicate the window). This is what actually un-zombies shotlog and restores its icon.+ try {+ const st = await rescanProfile(browser, profileName, { adoptOrphans: true });+ handledByRescan.add(profileName);+ for (const [sid, s] of sessions) {+ if (s.profileName === profileName && !s._lostBrowser && (s.tabs || []).length) {+ s._lastCat = undefined; // force a real category re-evaluation+ refreshWindowChrome(s, sid).catch(() => {});+ }+ }+ if (st && (st.reattached || st.adopted)) reclaimed += (st.reattached + st.adopted);+ } catch (e) { console.log(`[reclaim] rescan of "${profileName}" threw: ${e.message}`); } } catch (e) { continue; } } // Part A: any page in a connected profile that no session owns. for (const [profileName, be] of [...browsers]) {+ if (handledByRescan.has(profileName)) continue; // rescanProfile already reconciled this one // v1.9.50: NEVER reclaim from a profile with a launch in flight. A launching window exists as // a page for a moment BEFORE its session is registered, so a sweep landing in that gap sees a // legitimate window as unowned and adopts it — producing TWO sessions for ONE window (caught@@ -7085,14 +7128,21 @@ const server = http.createServer(async (req, res) => { return; } - // No sessionId — close everything (legacy behavior)+ // No sessionId — close everything (also the taskbar jump-list "Close ALL" task). for (const [pName, be] of browsers) { await killBrowserProcess(be.browser, pName); } browsers.clear();- for (const [id] of sessions) { deleteSessionFile(id); }+ // v1.9.51: unregister every per-session AUMID as we drop the sessions, so closing all+ // windows leaves NO stale taskbar identity behind (the exact "old icon" class John flagged).+ for (const [id, s] of sessions) {+ deleteSessionFile(id);+ try { if (s && s._curAppId && s._curAppId !== PUP_APP_ID) unregisterPupAumid(s._curAppId).catch(() => {}); } catch (e) {}+ } sessions.clear(); activeSessionId = null;+ // Belt-and-braces: sweep any orphaned Adom.Pup.* keys now that nothing is live.+ try { pruneStaleAumids(); } catch (e) {} sendJSON(res, { success: true, output: JSON.stringify({ ok: true, message: 'All sessions and browsers closed',