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
v1.9.3: per-window wiki view toggle in the taskbar jump list (browser_wiki_set_view + desktop_set_window_jumplist #208)
5 files changed
+150−8
CHANGELOG.md+13@@ -1,5 +1,18 @@ # Changelog — Adom Desktop Puppeteer Bridge +## 1.9.3 — 2026-07-19 — Per-window wiki view toggle in the taskbar jump list (#208 shipped)++AD 1.9.139+ shipped #208 (desktop_set_window_jumplist). Every WIKI pup window now carries a+taskbar RIGHT-CLICK task to flip between the logged-in and public view — "🔓 Switch to logged-in+view" on a public window, "📖 Switch to public view" on an authed one. The jump list attaches to+the per-session AUMID (registry-only, from 1.9.2), so it's a PER-WINDOW menu. Clicking it runs+AD's bundled CLI → the new browser_wiki_set_view verb, which relaunches THAT window under the+other cookie jar (profile = userDataDir is fixed at launch, so a view switch is a relaunch) and+brings it on-screen (a user click, so foregrounding is warranted). The menu is added/cleared on+open and on every navigation (drops off when a window leaves Adom); split mode only (grouped mode+shares one button); self-gates on older AD. If the authed jar hasn't done the one-time SSO yet,+the response flags wikiLoginNeeded and the user logs in once in that window.+ ## 1.9.2 — 2026-07-19 — Register per-session AUMIDs (#207 shipped): Adom icon in alt-tab + jumplist AD 1.9.136+ shipped #207 (desktop_register_app_identity {shortcut:false} + unregister). pup now
adom-bridge-puppeteer-manifest.json+5−5@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "1.9.2",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.2/adom-bridge-puppeteer-v1.9.2.zip",- "sha256": "3cacbd35ea273507b325a733c6dfe00cf7d2d18c67de6c4e917b3a86ea561aca",- "size": 209828,+ "version": "1.9.3",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.3/adom-bridge-puppeteer-v1.9.3.zip",+ "sha256": "e664508c4277458ee509e83cd58f8c70e5c99e73233a4741bee800d6606a8901",+ "size": 212553, "verbPrefixes": [ "browser_", "desktop_recorder_",@@ -12,5 +12,5 @@ ], "healthEndpoint": "/status", "statusVerb": "browser_readiness",- "released_at": "2026-07-19T00:00:00Z"+ "released_at": "2026-07-19T19:30:00Z" }
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.2+1.9.3
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "1.9.2",+ "version": "1.9.3", "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+130−1@@ -1658,6 +1658,7 @@ const PUP_APP_ID = 'Adom.Pup'; const PUP_DISPLAY_NAME = 'Adom Pup'; let _identitySupported = null; // null = unprobed let _identityRegistered = false;+let _jumplistSupported = null; // null = unprobed (desktop_set_window_jumplist, AD ≥1.9.139) // v1.8.74: adom-pup-BROWSER.ico — favicon + browser-window inlay (lower-right), so the icon // says "this is an Adom BROWSER window" at a glance. NEW FILENAME on art change, always — // Windows' shell + AD's HICON cache key by path (stale-tile risk otherwise). Future home for@@ -1737,6 +1738,41 @@ async function unregisterPerSessionAumid(sessionId) { _registeredSessionAumids.delete(appId); try { await chrome.adCommand('desktop_unregister_app_identity', { appId }, { timeoutMs: 5000 }); } catch (e) {} }+// v1.9.3 (AD #208 — desktop_set_window_jumplist, AD ≥1.9.139): give each WIKI pup window a+// taskbar RIGHT-CLICK task to flip between the logged-in and public view. The jump list attaches+// to the per-session AUMID (registered registry-only in v1.9.2), so it is a PER-WINDOW menu.+// Clicking a task runs AD's bundled CLI → browser_wiki_set_view, which relaunches THIS window+// under the other cookie jar (profile = userDataDir is fixed at browser launch, so a view switch+// = a relaunch). Only meaningful in SPLIT mode (grouped mode shares one taskbar button, so a+// per-window menu would be ambiguous) and only for Adom (adom.inc) URLs; any other URL clears the+// menu. Self-gates off if AD lacks the verb. Idempotent per (session, view) via _wikiJumplistView.+async function updateWikiJumplist(session, sessionId) {+ if (process.platform !== 'win32' || _identitySupported !== true || _jumplistSupported === false) return;+ if (!session || pupSettings().taskbarGrouping === 'grouped') return;+ const appId = `${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;+ if (session._wikiJumplistView === view) return; // menu already reflects this state+ let tasks = [];+ if (view === 'authed') {+ tasks = [{ title: '\u{1F4D6} Switch to public view', description: 'Reload this wiki page logged out — what the world sees', args: `browser_wiki_set_view {"sessionId":"${sessionId}","view":"public"}` }];+ } else if (view === 'public') {+ tasks = [{ title: '\u{1F513} Switch to logged-in view', description: 'Reload this wiki page signed in — private source, drafts, owner cards', args: `browser_wiki_set_view {"sessionId":"${sessionId}","view":"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 && !_registeredSessionAumids.has(appId)) {+ await registerPerSessionAumid(appId);+ if (!_registeredSessionAumids.has(appId)) return;+ }+ try {+ const r = await chrome.adCommand('desktop_set_window_jumplist', { appId, tasks }, { timeoutMs: 6000 });+ const raw = r && (typeof r.output === 'string' ? r.output : JSON.stringify(r.output || ''));+ if (_isUnknownVerb(r, raw)) { _jumplistSupported = false; return; }+ _jumplistSupported = true;+ session._wikiJumplistView = view;+ } catch (e) {}+} // v1.8.80: TAB-COUNT overlay badge (John: "if the ai opens 8 tabs that makes that pup window // unique"). The overlay slot is FREE on identity-capable AD (the base icon is the Adom Pup // identity), so it now shows the window's tab count for >=2 tabs — dark circle, white digit,@@ -1918,7 +1954,7 @@ function wikiViewHint(url, view, auth) { return ` 🔑 Wiki authed view requested, but NO ONE has done the one-time login yet (or the ~30-day session expired) — so this window is currently PUBLIC/logged-out. A thread can't self-login (it's the user's SSO). Offer it: browser_raise_os_window {foregroundReason:"sign into the Adom wiki"} → user does the hydrogen.adom.inc SSO ONCE → then EVERY thread's wikiView:"authed" is silently logged in for ~30 days (shared vault profile). Tell the user it's a one-time login that unlocks the logged-in view everywhere.`; } // public/default open — the 80% case. One line, informational.- return ` (📖 Wiki PUBLIC view — the default, logged out, what the world sees. If the task needs the user's LOGGED-IN view (private source, drafts, owner cards, replying), reopen with wikiView:"authed" — if the user has logged in once, it's INSTANT and shared across all threads; if not, pup offers a one-time login. Ignore otherwise.)`;+ return ` (📖 Wiki PUBLIC view — the default, logged out, what the world sees. If the task needs the user's LOGGED-IN view (private source, drafts, owner cards, replying), reopen with wikiView:"authed" — if the user has logged in once, it's INSTANT and shared across all threads; if not, pup offers a one-time login. The USER can also flip this window themselves via its taskbar right-click menu ("🔓 Switch to logged-in view"). Ignore otherwise.)`; } async function fetchPageFavicon(page) {@@ -4097,6 +4133,7 @@ const server = http.createServer(async (req, res) => { V('browser_open_tab', 'Open a new tab in an EXISTING session/window (returns {tabId, tabCount, activeTabId} — no need to diff list_tabs). This is the right verb for multi-URL work: ONE window with many tabs (one taskbar button), not many windows. The window stays in the background; the tab auto-flashes the taskbar (debounced) so the user notices. The USER can click the window\'s taskbar button at any time to view it — pup never pulls it back down.', { sessionId: 'required str', url: 'optional str', updateNote: 'optional str — short "what I did" note surfaced in list_tabs as lastAgentUpdate', silent: 'optional bool — suppress the auto-flash for this call' }, 30, { sessionId: 'work', url: 'https://example.com' }), V('browser_close_tab', 'Close a tab.', { sessionId: 'required str', tabId: 'optional str (default active)' }, 15, { sessionId: 'work' }), V('browser_close_window', 'Close a window/session (its tabs go too).', { sessionId: 'required str' }, 20, { sessionId: 'work' }),+ V('browser_wiki_set_view', 'Flip a WIKI pup window between the logged-in and public view IN PLACE (relaunches the same window under the other cookie jar). This is the callback for the per-window taskbar right-click jump-list task — it is a USER-initiated action (the window is brought on-screen). AI threads should NOT call this; pass wikiView:"authed"|"public" on browser_open_window instead.', { sessionId: 'required str', view: 'required "authed"|"public"' }, 45, { sessionId: 'wiki-bmv080', view: 'authed' }), V('browser_navigate', 'Navigate the active tab to a URL.', { sessionId: 'required str', url: 'required str' }, 45, { sessionId: 'work', url: 'https://example.com' }), V('browser_back', 'History back.', { sessionId: 'required str' }, 20, { sessionId: 'work' }), V('browser_forward', 'History forward.', { sessionId: 'required str' }, 20, { sessionId: 'work' }),@@ -4342,6 +4379,13 @@ const server = http.createServer(async (req, res) => { // v1.8.26: record whether this window is meant to be on screen — gates every // later bringToFront (navigate/reload/switch_tab must not raise a bg window). if (session) session._foreground = foreground;+ // v1.9.3: remember this window's wiki view so the taskbar jump-list toggle (and any+ // later relaunch) knows which cookie jar it's on. Record the "public" profile too so+ // toggling back from authed restores the original jar, not just the sessionId default.+ if (session && isAdomUrl(args.url)) {+ session._wikiView = (args.wikiView === 'authed') ? 'authed' : 'public';+ if (session._wikiView === 'public') session._publicProfile = session.profileName;+ } if (session && fgGranted) { session._lastForeground = { ts: Date.now(), verb: 'browser_open_window', reason: fgReason }; try { recordAgentUpdate(session, 'browser_open_window', { updateNote: `FOREGROUND: ${fgReason}` }, session.activeTabId); } catch (e) {}@@ -4360,6 +4404,9 @@ const server = http.createServer(async (req, res) => { // the REUSED-window path (no park runs there, but the badge may have been cleared). if (process.platform === 'win32') { setTimeout(() => { brandPupWindow(sessionId).catch(() => {}); }, 300);+ // v1.9.3: attach/clear the per-window wiki view-toggle jump list once the window's+ // identity is stamped (registers the per-session AUMID if needed).+ setTimeout(() => { updateWikiJumplist(session, sessionId).catch(() => {}); }, 900); } // Verbose browser report: which browser we actually drove, what else is available. const brep = browserPickReport();@@ -4553,6 +4600,85 @@ const server = http.createServer(async (req, res) => { return; } + // v1.9.3 (AD #208): the taskbar jump-list callback — flip THIS wiki window between the+ // logged-in and public view IN PLACE. A view switch = a cookie-jar (userDataDir) switch,+ // fixed at browser launch, so it's a close-and-relaunch of the same sessionId under the+ // other profile at the same URL. Invoked when the user clicks the per-window taskbar+ // right-click task (runs AD's bundled CLI → this verb); it's a USER action, so the window+ // is brought on-screen. Not meant for AI threads (they pass wikiView on browser_open_window).+ case 'browser_wiki_set_view': {+ const sessionId = args.sessionId;+ const view = args.view === 'authed' ? 'authed' : 'public';+ if (!sessionId || !sessions.has(sessionId)) { sendJSON(res, { success: false, error: `Session "${sessionId}" not found` }); return; }+ const session = sessions.get(sessionId);+ let url = null; try { url = session.page.url() || null; } catch (e) {}+ if (!url || !isAdomUrl(url)) { sendJSON(res, { success: false, error: 'wiki view toggle applies only to Adom (adom.inc) URLs', output: JSON.stringify({ ok: false, url }) }); return; }+ const curView = session._wikiView === 'authed' ? 'authed' : 'public';+ const owner = session.owner || null;+ const publicProfile = session._publicProfile || sessionId;+ if (curView === view) {+ // Already in the requested view — just surface it (user clicked to see this window).+ session._foreground = true;+ try { await session.page.bringToFront(); } catch (e) {}+ sendJSON(res, { success: true, output: JSON.stringify({ ok: true, view, unchanged: true, url }) });+ return;+ }+ console.log(`[wiki] view toggle "${sessionId}": ${curView} → ${view} (${url})`);+ // Close the current window (mirror browser_close_window) so we can relaunch under the+ // other cookie jar.+ const oldProfile = session.profileName;+ for (const t of [...session.tabs]) { try { await t.page.close(); } catch (e) {} }+ session.tabs.length = 0; session.activeTabId = null;+ sessions.delete(sessionId);+ deleteSessionFile(sessionId);+ unregisterPerSessionAumid(sessionId).catch(() => {});+ const be = browsers.get(oldProfile);+ if (be) {+ be.refCount = Math.max(0, be.refCount - 1);+ if (be.refCount <= 0) { try { await killBrowserProcess(be.browser, oldProfile); } catch (e) {} browsers.delete(oldProfile); }+ }+ // Relaunch the SAME sessionId under the new profile at the same URL.+ const newProfile = view === 'authed' ? WIKI_AUTH_PROFILE : publicProfile;+ let s2 = null;+ try { s2 = await launchSession(sessionId, url, { profile: newProfile, owner, takeover: true, foreground: true }); }+ catch (e) { sendJSON(res, { success: false, error: `wiki view relaunch failed: ${e.message}` }); return; }+ s2._wikiView = view;+ if (view === 'public') s2._publicProfile = newProfile;+ s2._foreground = true;+ s2._wikiJumplistView = undefined;+ // The user clicked a per-window taskbar task → bring it on-screen (CDP place+maximize,+ // then bringToFront — same on-screen move as browser_raise_os_window, no PowerShell).+ try {+ const cdp = await s2.page.target().createCDPSession();+ const { windowId } = await cdp.send('Browser.getWindowForTarget');+ await cdp.send('Browser.setWindowBounds', { windowId, bounds: { left: 0, top: 0, width: 1280, height: 800, windowState: 'normal' } });+ await cdp.send('Browser.setWindowBounds', { windowId, bounds: { windowState: 'maximized' } });+ } catch (e) {}+ try { await s2.page.bringToFront(); } catch (e) {}+ s2._lastForeground = { ts: Date.now(), verb: 'browser_wiki_set_view', reason: `user clicked the taskbar "switch to ${view} view" task` };+ try { recordAgentUpdate(s2, 'browser_wiki_set_view', { updateNote: `wiki view → ${view} (user clicked taskbar task)` }, s2.activeTabId); } catch (e) {}+ // Re-check auth (authed view only) + refresh the in-tab glyph, branding, and jump list.+ let authState = null;+ if (view === 'authed') { try { authState = await Promise.race([checkWikiAuth(s2.page), new Promise(r => setTimeout(() => r({ authed: false }), 3500))]); } catch (e) {} }+ const label = (view === 'authed' && authState && authState.authed) ? '\u{1F513} Logged in' : '\u{1F4D6} Public';+ try { await setWikiModeGlyph(s2.page, label); } catch (e) {}+ if (process.platform === 'win32') {+ try { await brandPupWindow(sessionId); } catch (e) {}+ try { await registerPerSessionAumid(`${PUP_APP_ID}.${sessionId}`); } catch (e) {}+ try { await applyAppOverlay(s2, sessionId); } catch (e) {}+ await updateWikiJumplist(s2, sessionId).catch(() => {});+ }+ const loginNeeded = view === 'authed' && authState && !authState.authed;+ sendJSON(res, { success: true, output: JSON.stringify({+ ok: true, view, url, foregrounded: true,+ wikiLoginNeeded: loginNeeded || undefined,+ _hint: loginNeeded+ ? 'Switched to the logged-in profile, but no one has done the one-time SSO login yet (or the ~30-day session expired) — so this window still shows the PUBLIC/logged-out view. The user logs in ONCE at hydrogen.adom.inc in this window; then every thread\'s authed view is signed in for ~30 days.'+ : `This window is now the ${view === 'authed' ? 'LOGGED-IN' : 'PUBLIC'} view.`,+ }) });+ return;+ }+ // --- Tab-level commands --- case 'browser_open_tab': { const sessionId = args.sessionId || activeSessionId;@@ -5807,6 +5933,9 @@ const server = http.createServer(async (req, res) => { } catch (e) {} }, 1800); }+ // v1.9.3: refresh the per-window wiki jump list (adds the toggle on a wiki page,+ // clears it when the window navigated off Adom).+ setTimeout(() => { updateWikiJumplist(session, resolvedId).catch(() => {}); }, 1000); } sendJSON(res, { success: true, output: JSON.stringify({ ok: true, url: args.url, sessionId: resolvedId, tabId, ...(authSteerHint(args.url) ? { _hint: authSteerHint(args.url).trim() } : {}) }) }); return;