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
Issue #28 (Drew Owens) — browser_set_viewport no longer pins a window for life. All four of his requested fixes: (1) RESET FORM — {reset:true} (or omitting width/height) calls setViewport(null), clearing the Emulation device-metrics override so the window tracks the real OS window again; previously width/height were required ints with no null, no reset, and the only escape was closing the window, which on a shared Chrome can destroy other threads' windows. (2) STATE SURFACED — emulatedViewport {width,height,deviceScaleFactor}|null now appears in browser_status and browser_list_windows, so a window that ignores OS resizing is visibly in that state instead of being indistinguishable from an app with broken layout code (which cost Drew a real debugging session hunting his own resize handlers). (3) HONEST HINT — the response now states the LIFECYCLE up front: the pin survives navigation AND hard reload, the window stops tracking OS resizing while set, and it names the exact reset call; the old hint sold the feature's benefit and never mentioned the effect was permanent. (4) DOCUMENTED in browser_describe with the same warning, so an agent reading the catalog sees the trap before walking into it.
4 files changed
+36−8
adom-bridge-puppeteer-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "1.9.218",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.218/adom-bridge-puppeteer-v1.9.218.zip",- "sha256": "341f5a135ed65892de01f365491a044b58ce23b54c2aa916585b8a6eb2e6b5ce",- "size": 444459,+ "version": "1.9.219",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.219/adom-bridge-puppeteer-v1.9.219.zip",+ "sha256": "0d2eacb58abce46feafb5dc4822a3ecea4078b9ab4342e0ce562ad761d5da377",+ "size": 445693, "verbPrefixes": [ "browser_", "desktop_recorder_",
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.218+1.9.219
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "1.9.218",+ "version": "1.9.219", "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. 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, browser_alert_window posts a native toast whose click focuses the window, browser_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-desktop", "author": "Adom Inc.",
src/server.js+30−2@@ -7629,6 +7629,7 @@ function getSessionInfoFast(sessionId, session) { ageMinutes: session.createdAt ? Math.round((Date.now() - session.createdAt) / 60000) : null, lastAgentUpdate: session._lastAgentUpdate || null, lastForeground: session._lastForeground || null,+ emulatedViewport: session._emulatedViewport || null, degraded: true, degradedReason: 'this window was busy (Chrome under heavy load) so pup returned its LAST-KNOWN state instead of stalling — the window is NOT crashed; retry shortly for live values', };@@ -7670,6 +7671,9 @@ async function getSessionInfo(sessionId, session) { // v1.8.70: who put this window on the user's screen, when, and their stated reason — // the audit answer to "why did this foreground?" in one status query. lastForeground: session._lastForeground || null,+ // v1.9.219 (issue #28): report a pinned viewport. A window that ignores OS resizing must be+ // visibly in that state — otherwise it is indistinguishable from an app with broken layout code.+ emulatedViewport: session._emulatedViewport || null, background: (() => { try { const bg = browsers.get(session.profileName)?.browser?._bgResult || null;@@ -7863,6 +7867,8 @@ const server = http.createServer(async (req, res) => { V('browser_type', 'Type text into a field. Pass clear:true to REPLACE the existing value instead of appending (this is what you want on any prefilled/React form — it selects-all then deletes with real key events, which React-controlled inputs honour, unlike setting .value).', { sessionId: 'required str', text: 'required str', selector: 'optional str', clear: 'optional bool — empty the field first (replace, not append)' }, 20, { sessionId: 'work', selector: '#user', text: 'adominc1', clear: true }), V('browser_press_key', 'Press a key, optionally with modifier chords held (Ctrl+A, Ctrl+V, Shift+Tab).', { sessionId: 'required str', key: 'required str', modifiers: 'optional array — any of ["Control","Shift","Alt","Meta"] (aliases ctrl/cmd/command/option accepted); held down around the key press' }, 15, { sessionId: 'work', key: 'a', modifiers: ['Control'] }), V('browser_NATIVE_INPUT_WARNING', '⛔ READ BEFORE USING AD\'s desktop_click / desktop_type / desktop_press_key ON A PUP WINDOW. Native OS input does NOT target a window — it goes to WHATEVER WINDOW HAS OS FOCUS at that instant. This user runs many AI threads and alt-tabs away from a foregrounded window almost immediately. If you fire native input without warning them, your keystrokes land in THEIR document (this happened: a username was typed into the user\'s own window). REQUIRED: notify_user toast + desktop_caption FIRST, tell them explicitly "don\'t type or click until I say done", wait until the window is genuinely foreground, re-verify immediately before EACH event, and caption when done. browser_assisted_click already does all of this — use it. And ALWAYS prefer CDP input (browser_click/browser_type/browser_hover): it needs no foreground and disturbs nobody. A native verb returning ok:true means the event was dispatched to the OS, NOT that it reached your window.', {}, 0, {}),+ V('browser_set_viewport', 'Pin the page viewport (CDP device-metrics) to an exact size for deterministic screenshots — OR clear that pin. ⚠ A pin PERSISTS: it survives navigation and hard reload, and while set the window stops tracking OS resizing (a human resizing it sees no reflow, which looks exactly like an app layout bug). ALWAYS clear it when done. Reported as emulatedViewport in browser_status / browser_list_windows.', { sessionId: 'required str', width: 'int — required unless reset', height: 'int — required unless reset', deviceScaleFactor: 'optional number (default 1)', reset: 'optional bool — CLEAR the override and track the OS window again (width/height not needed)' }, 15, { sessionId: 'work', reset: true }),+ V('browser_deps', 'Report pup dependency health (wiki issue #15): depsPresent, selfInstalledThisBoot, lastError. Use this to answer "is pup broken?" without reading spawn logs.', { }, 10, {}), V('browser_hover', 'Move the pointer over a target WITHOUT clicking (reveals tooltips / hover menus). Glides the visible Adom cursor there too.', { sessionId: 'required str', selector: 'optional str', x: 'optional int', y: 'optional int', intent: 'optional str — narrated on the cursor label', steps: 'optional int (default 8)' }, 15, { sessionId: 'work', selector: '#menu' }), V('browser_maximize', 'Maximize (or restore/fullscreen/minimize) a pup window via CDP — no AD desktop verb required. Does NOT foreground it.', { sessionId: 'required str', state: 'optional "maximized"|"normal"|"minimized"|"fullscreen" (default maximized)' }, 15, { sessionId: 'work' }), V('browser_screenshot', 'Screenshot the page (PNG; fullPage optional). NOTE: fullPage only covers DOCUMENT-level scroll — on SPAs that scroll inside a nested container it returns just the viewport + a nestedScroll hint; use browser_screenshot_element or browser_scroll for those.', { sessionId: 'required str', fullPage: 'optional bool' }, 30, { sessionId: 'work' }),@@ -9446,16 +9452,38 @@ const server = http.createServer(async (req, res) => { let tab, tabId; try { ({ tab, tabId } = resolveTabOrError(session, resolvedId, args)); } catch (e) { sendTabResolveError(res, e); return; }+ // v1.9.219 (wiki issue #28, Drew Owens): the override used to PIN a window for life — it+ // survived a hard reload, could not be cleared, and nothing reported it, so a window that+ // silently stopped tracking OS resizing was indistinguishable from an app with broken layout+ // code. That cost him a real debugging session hunting his own resize handlers. Fix 1 of 4:+ // a RESET form. Pass {reset:true} (or omit width/height) to call setViewport(null), which+ // clears Emulation device-metrics and hands the page back to the real OS window.+ const wantsReset = args.reset === true || (args.width === undefined && args.height === undefined);+ if (wantsReset) {+ try {+ await tab.page.setViewport(null);+ session._emulatedViewport = null;+ const dims = await tab.page.evaluate(() => ({ innerWidth: window.innerWidth, innerHeight: window.innerHeight, devicePixelRatio: window.devicePixelRatio }));+ sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId: resolvedId, tabId, reset: true, ...dims,+ _hint: `Viewport override CLEARED — this window tracks the real OS window again (innerWidth now ${dims.innerWidth}, dpr ${dims.devicePixelRatio}). Resizing the window will reflow the page normally.` }) });+ } catch (e) { sendJSON(res, { success: false, error: `clearing the viewport override failed: ${e.message}` }); }+ return;+ } const vw = Number(args.width), vh = Number(args.height); if (!Number.isFinite(vw) || !Number.isFinite(vh)) {- sendJSON(res, { success: false, error: 'browser_set_viewport requires numeric width + height.', _hint: 'e.g. browser_set_viewport {"sessionId":"...","width":1280,"height":900}. Optional deviceScaleFactor (default 1).' });+ sendJSON(res, { success: false, error: 'browser_set_viewport requires numeric width + height (or {reset:true} to clear an existing override).', _hint: 'Set: {"sessionId":"...","width":1280,"height":900}. Clear: {"sessionId":"...","reset":true}. Optional deviceScaleFactor (default 1).' }); return; } const dsf = Number(args.deviceScaleFactor) || 1; try { await tab.page.setViewport({ width: Math.round(vw), height: Math.round(vh), deviceScaleFactor: dsf });+ // Fix 2 of 4: RECORD the override so browser_status / browser_list_windows can report it.+ session._emulatedViewport = { width: Math.round(vw), height: Math.round(vh), deviceScaleFactor: dsf }; const dims = await tab.page.evaluate(() => ({ innerWidth: window.innerWidth, innerHeight: window.innerHeight, devicePixelRatio: window.devicePixelRatio }));- sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId: resolvedId, tabId, applied: { width: Math.round(vw), height: Math.round(vh), deviceScaleFactor: dsf }, ...dims, _hint: `Viewport set to ${Math.round(vw)}×${Math.round(vh)} (innerWidth now ${dims.innerWidth}). Now browser_screenshot / browser_screenshot_element render at this size.` }) });+ // Fix 3 of 4: the hint must state the LIFECYCLE, not just the benefit. The old text sold+ // the feature and never said the effect is permanent for the life of the window.+ sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId: resolvedId, tabId, applied: { width: Math.round(vw), height: Math.round(vh), deviceScaleFactor: dsf }, ...dims,+ _hint: `Viewport PINNED to ${Math.round(vw)}×${Math.round(vh)} (innerWidth now ${dims.innerWidth}). ⚠ THIS PERSISTS: it survives navigation AND hard reload, and while it is set the window NO LONGER TRACKS OS RESIZING — a human resizing this window will see the page not reflow, which reads exactly like an app layout bug. CLEAR IT when you are done: browser_set_viewport {"sessionId":"${resolvedId}","reset":true}. It is reported as emulatedViewport in browser_status / browser_list_windows.` }) }); } catch (e) { sendJSON(res, { success: false, error: `setViewport failed: ${e.message}` }); } return; }