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.27: category-carrying per-session appIds so a category flip creates a fresh taskbar button with the new tile
5 files changed
+75−43
CHANGELOG.md+13@@ -1,5 +1,18 @@ # Changelog — Adom Desktop Puppeteer Bridge +## 1.9.27 — 2026-07-20 — Live tile repaints via category-carrying appIds (AD's measured fix)++AD measured every repaint mechanism on Win11 (WIN11_TASKBAR_ICON.md Part 7): the taskbar tile is+baked when the button is created and never re-read - WM_SETICON, DeleteTab/AddTab, SHChangeNotify,+FRAMECHANGED, RedrawWindow and the TaskbarCreated broadcast all leave it unchanged. The only live+repaint is making the shell create a NEW button, which a DIFFERENT appId does (~1s, no flicker).+So the per-session appId now carries the category: Adom.Pup.<sid>.<category>. A category flip is+register-new, stamp, unregister-old; the jump list re-attaches to the new AUMID automatically.+Registrations are keyed by the full appId (a Set - state is baked into the id). Session teardown+captures the current appId before the session record is deleted so cleanup cannot leak. Tradeoff,+by Windows design: pins bind to one AUMID, so pins do not follow category flips on per-session+windows; grouped mode keeps the stable pinnable Adom.Pup.+ ## 1.9.26 — 2026-07-20 — Signed-in icon applies on the FIRST stamp (Windows latches button icons) Proven live: ex-wikiin stamped and registered icon=wiki-in, yet its taskbar button kept the plain
adom-bridge-puppeteer-manifest.json+5−5@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "1.9.26",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.26/adom-bridge-puppeteer-v1.9.26.zip",- "sha256": "ff4d4034a582fbf63a0ec1c5228c8910ba4246cd43a31b7252068f21265f8a25",- "size": 281278,+ "version": "1.9.27",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.27/adom-bridge-puppeteer-v1.9.27.zip",+ "sha256": "9f0ff74667eba3fa37c2311321ce2b917a83a20fe22eccbdc359381a95993fde",+ "size": 281747, "verbPrefixes": [ "browser_", "desktop_recorder_",@@ -12,5 +12,5 @@ ], "healthEndpoint": "/status", "statusVerb": "browser_readiness",- "released_at": "2026-07-20T04:45:00Z"+ "released_at": "2026-07-20T05:30:00Z" }
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.26+1.9.27
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "1.9.26",+ "version": "1.9.27", "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+55−36@@ -1795,9 +1795,28 @@ async function stampPupIdentity(sessionId) { // window; the per-window RelaunchIconResource/RelaunchDisplayNameResource AD sets are what // brand an UNREGISTERED AUMID's button, and the registered plain "Adom.Pup" stays as the // pinnable Start Menu launcher.+ // v1.9.27 (AD's Win11 measurement, WIN11_TASKBAR_ICON.md Part 7): the taskbar TILE is baked+ // when the BUTTON is created and NEVER re-read — WM_SETICON, DeleteTab/AddTab, SHChangeNotify,+ // FRAMECHANGED, RedrawWindow, TaskbarCreated all leave it unchanged. The ONLY live repaint is+ // making the shell create a NEW button, which happens when the window is stamped with a+ // DIFFERENT appId. So the per-session appId now CARRIES THE CATEGORY:+ // Adom.Pup.<sid>.<category>+ // A category flip = register new appId -> stamp it -> unregister the old one (~1s, no flicker,+ // AD-verified). TRADEOFF (Windows constraint, by design): a taskbar PIN binds to one AUMID, so+ // a pin made under an old category shows the running window as a separate button. Per-session+ // windows are transient and their pins launch via relaunchCommand anyway, so tile-tracks-state+ // wins here; GROUPED mode keeps the stable pinnable Adom.Pup.+ const grouped = pupSettings().taskbarGrouping === 'grouped';+ const cat = pupCategory(sessionId) || 'plain';+ const appId = grouped ? PUP_APP_ID : `${PUP_APP_ID}.${sessionId}.${cat}`;+ const sess = sessions.get(sessionId);+ const prevAppId = sess && sess._curAppId;+ // Register the NEW appId BEFORE stamping (AD's verified order) so the fresh button resolves+ // its icon/name from the registry the moment it is created.+ if (!grouped && prevAppId !== appId) { try { await registerPupAumid(appId, sessionId); } catch (e) {} } const r = await chrome.adCommand('desktop_set_window_identity', {- titleContains: `(session: ${sessionId}`, cacheKey: `pup-${sessionId}-${pupCategory(sessionId) || 'plain'}`,- appId: pupSettings().taskbarGrouping === 'grouped' ? PUP_APP_ID : `${PUP_APP_ID}.${sessionId}`, displayName: PUP_DISPLAY_NAME, iconPath: pupIconPath(sessionId),+ titleContains: `(session: ${sessionId}`, cacheKey: `pup-${sessionId}-${cat}`,+ appId, displayName: PUP_DISPLAY_NAME, iconPath: pupIconPath(sessionId), // v1.9.11 (AD ≥1.9.141): brand the taskbar RIGHT-CLICK MENU HEADER row (it showed the // chrome.exe icon for months). There is NO "ApplicationIcon" registry value on AUMID keys // and the registry cannot brand the header at all — the header obeys the window's@@ -1817,43 +1836,42 @@ async function stampPupIdentity(sessionId) { const o = raw ? JSON.parse(raw) : {}; const data = o.data || o; const ok = !!(data.ok || (data.applied && data.applied.length));- if (ok) console.log(`[identity] "${sessionId}" stamped: ${(data.applied || []).join(',') || 'ok'}`);- // v1.9.2 (#207, AD ≥1.9.136): REGISTER the per-session AUMID registry-only (shortcut:false)- // so Alt-Tab + the taskbar right-click jumplist header resolve the Adom icon/name instead of- // falling back to chrome.exe. Only the transient split-mode per-session id needs this; the- // base "Adom.Pup" is already registered WITH a shortcut (the pinnable launcher). Unregistered- // on session close (registerPerSessionAumid tracks it). Self-gates if the verb is absent.- if (ok && pupSettings().taskbarGrouping !== 'grouped') {- registerPerSessionAumid(`${PUP_APP_ID}.${sessionId}`).catch(() => {});+ if (ok) {+ console.log(`[identity] "${sessionId}" stamped appId=${appId}: ${(data.applied || []).join(',') || 'ok'}`);+ if (sess && !grouped && prevAppId !== appId) {+ sess._curAppId = appId;+ // The jump list attaches to the AUMID — a new appId starts with NO jump list, so force a+ // re-attach on the next updateWikiJumplist pass.+ sess._wikiJumplistView = undefined;+ updateWikiJumplist(sess, sessionId).catch(() => {});+ // Unregister the superseded appId (cleanup, AD step 3). Never the pinnable base id.+ if (prevAppId && prevAppId !== PUP_APP_ID) { unregisterPupAumid(prevAppId).catch(() => {}); }+ } } return ok; } catch (e) { return false; } }-// v1.9.12: Map (not Set) appId -> the ICON STATE it was registered with ('auth' | 'plain'). The-// registration is what Alt-Tab and the flyout header resolve, so a wiki login/logout must-// RE-register with the other icon — a plain "already registered" guard would pin the stale one.-const _registeredSessionAumids = new Map();-async function registerPerSessionAumid(appId) {- if (_identitySupported === false || appId === PUP_APP_ID) return;- const sid = appId.startsWith(PUP_APP_ID + '.') ? appId.slice(PUP_APP_ID.length + 1) : null;- const state = pupCategory(sid) || 'plain';- if (_registeredSessionAumids.get(appId) === state) return; // already registered in THIS state+// v1.9.27: registrations are keyed by the FULL category-carrying appId. The category is baked+// into the id itself (Adom.Pup.<sid>.<cat>), so a Set suffices — a state change is a NEW id.+const _registeredPupAumids = new Set();+async function registerPupAumid(appId, sessionId) {+ if (_identitySupported === false || appId === PUP_APP_ID || _registeredPupAumids.has(appId)) return; try { const r = await chrome.adCommand('desktop_register_app_identity', {- appId, displayName: PUP_DISPLAY_NAME, iconPath: pupIconPath(sid), shortcut: false,+ appId, displayName: PUP_DISPLAY_NAME, iconPath: pupIconPath(sessionId), shortcut: false, }, { timeoutMs: 6000 }); const raw = r && (typeof r.output === 'string' ? r.output : JSON.stringify(r.output || '')); if (_isUnknownVerb(r, raw)) return; // older AD without #207 — the per-window stamp still brands the taskbar button- _registeredSessionAumids.set(appId, state);- console.log(`[identity] registered per-session AUMID ${appId} (registry-only, icon=${state}) — Alt-Tab/jumplist show the Adom icon`);+ _registeredPupAumids.add(appId);+ console.log(`[identity] registered AUMID ${appId} (registry-only, icon=${pupCategory(sessionId) || 'plain'})`); } catch (e) {} }-async function unregisterPerSessionAumid(sessionId) {- const appId = `${PUP_APP_ID}.${sessionId}`;- if (process.platform !== 'win32' || !_registeredSessionAumids.has(appId)) return;- _registeredSessionAumids.delete(appId);+async function unregisterPupAumid(appId) {+ if (process.platform !== 'win32' || !appId || appId === PUP_APP_ID) return;+ _registeredPupAumids.delete(appId); try { await chrome.adCommand('desktop_unregister_app_identity', { appId }, { timeoutMs: 5000 }); } catch (e) {} }+ // v1.9.12: record the wiki auth state and RE-BRAND if it changed, so the taskbar icon flips // between the browser-window glyph and the solid padlock the moment login state is known. // Returns true if the state changed (i.e. a re-stamp was issued).@@ -1876,8 +1894,6 @@ async function setWikiAuthedState(session, sessionId, authed) { if (!stamped) await new Promise(r => setTimeout(r, 800 * (i + 1))); } if (!stamped) console.log(`[identity] "${sessionId}" icon re-stamp FAILED after retries — window title never matched`);- try { await registerPerSessionAumid(`${PUP_APP_ID}.${sessionId}`); } catch (e) {}- try { await updateTabCountBadge(session, sessionId); } catch (e) {} // repaint favicon on the new plate colour return true; } // v1.9.3 (AD #208 — desktop_set_window_jumplist, AD ≥1.9.139): give each WIKI pup window a@@ -1891,7 +1907,7 @@ async function setWikiAuthedState(session, sessionId, authed) { 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}`;+ 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; if (session._wikiJumplistView === view) return; // menu already reflects this state@@ -1916,9 +1932,9 @@ async function updateWikiJumplist(session, sessionId) { } // 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;+ if (tasks.length && !_registeredPupAumids.has(appId)) {+ await registerPupAumid(appId, sessionId);+ if (!_registeredPupAumids.has(appId)) return; } try { const r = await chrome.adCommand('desktop_set_window_jumplist', { appId, tasks }, { timeoutMs: 6000 });@@ -1987,8 +2003,10 @@ async function updateTabCountBadge(session, sessionId) { const cat = pupCategory(sessionId); if (cat !== session._lastCat) { session._lastCat = cat;+ // v1.9.27: the stamp now swaps to a category-carrying appId, which makes the shell create a+ // FRESH button with the new tile (the only repaint Win11 allows) and handles the AUMID+ // register/unregister + jump-list re-attach itself. brandPupWindow(sessionId).then(ok => { if (ok) session._badgedAt = Date.now(); }).catch(() => {});- registerPerSessionAumid(`${PUP_APP_ID}.${sessionId}`).catch(() => {}); } } catch (e) {} try {@@ -4918,6 +4936,7 @@ const server = http.createServer(async (req, res) => { const profileName = session.profileName; // Close ALL tabs in this session (a window can now hold many tabs)+ const _closingAppId = session._curAppId || `${PUP_APP_ID}.${sessionId}`; for (const t of [...session.tabs]) { try { await t.page.close(); } catch (e) {} }@@ -4925,7 +4944,7 @@ const server = http.createServer(async (req, res) => { session.activeTabId = null; sessions.delete(sessionId); deleteSessionFile(sessionId);- unregisterPerSessionAumid(sessionId).catch(() => {}); // v1.9.2 (#207): clean up the per-session AUMID registration+ unregisterPupAumid(_closingAppId).catch(() => {}); // v1.9.2/#207 cleanup (appId captured pre-delete) // Decrement refCount; kill browser if no sessions left using this profile const be = browsers.get(profileName);@@ -4981,11 +5000,12 @@ const server = http.createServer(async (req, res) => { // Close the current window (mirror browser_close_window) so we can relaunch under the // other cookie jar. const oldProfile = session.profileName;+ const _closingAppId = session._curAppId || `${PUP_APP_ID}.${sessionId}`; 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(() => {});+ unregisterPupAumid(_closingAppId).catch(() => {}); const be = browsers.get(oldProfile); if (be) { be.refCount = Math.max(0, be.refCount - 1);@@ -5027,7 +5047,6 @@ const server = http.createServer(async (req, res) => { // with title containing (session: wiki-demo)" ([badge] FAILED in the log). await new Promise(r => setTimeout(r, 1500)); try { await brandPupWindow(sessionId); } catch (e) {}- try { await registerPerSessionAumid(`${PUP_APP_ID}.${sessionId}`); } catch (e) {} try { await applyAppOverlay(s2, sessionId); } catch (e) {} try { await updateWikiJumplist(s2, sessionId); } catch (e) {} }