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 docs: Multi default and keys; gates refuse to run over other threads' windows
3 files changed
+113−6
docs/screenshots-and-recording.md+1−1@@ -29,4 +29,4 @@ ffmpeg -i take.mp4 -vf "fps=12,scale=800:-1" loop.gif ## Annotate -Annotate puts a drawing layer over a page: pen, arrows, boxes and text. Start it from a tile's jump list (Annotate this page), from the pen in pup's browser toolbar, with Alt+Shift+A, or by an AI calling `pup_annotate`. When you click Done, the marked-up region is captured, copied to your clipboard, and sent to the AI thread that owns the window, with a thin caption naming the thread, page and time. The drawing stays visible, so the AI's next screenshot shows it too.+Annotate puts a drawing layer over a page: pen, arrows, boxes and text. Start it from a tile's jump list (Annotate this page), from the pen in pup's browser toolbar, with Alt+Shift+A, or by an AI calling `pup_annotate`. It starts in **Multi** mode: draw as many shapes as you like (each in the next color), then press **Enter** or click Done. **Space** switches to **Single** mode, where each shape is captured the moment you lift the mouse (the quick red circle); **M** and **S** pick a mode directly and **Esc** cancels. A hint under the toolbar shows the keys. When you finish, the marked-up region is captured, copied to your clipboard, and sent to the AI thread that owns the window, with a thin caption naming the thread, page and time. The drawing stays visible, so the AI's next screenshot shows it too.
tools/foreground-proof.sh+13−5@@ -36,12 +36,20 @@ t=[x for x in w if not x.get('minimized') and x.get('kind')=='normal' and x.get( print(t[0].get('hwnd') if t else '')"); fi [ -n "$PIN" ] || { echo "no non-pup window to pin on $BOX"; exit 2; } echo "== pin window $PIN in front, force a cold lane =="-# every thread's windows, by name: the "everyone" close only clears the caller's own (measured), and a-# leftover window with the harness's name owned by another thread makes the open fail with not_your_window-for W in $(timeout 60 $AB pup_status '{}' 2>/dev/null | python3 -c "+# 2.0.700: NEVER touch another thread's windows. This used to close every window on the box with takeover, then+# force-kill the browser; on 2026-10-05 it closed another thread's adom-parts-search window on arav-rog for good.+# The gate needs a cold browser, which means ending every window in it, so a box where another thread has+# windows is not a valid box for it: refuse (INVALID, exit 2) and name them. Only the gate's own leftovers close.+SESS=$(timeout 60 $AB pup_status '{}' 2>/dev/null)+OTHERS=$(printf '%s' "$SESS" | python3 -c " import sys,json-try: print(' '.join(s.get('sessionId') for s in json.load(sys.stdin).get('sessions') or []))-except Exception: pass"); do timeout 60 $AB pup_close_window "{\"window\":\"$W\",\"takeover\":true,\"reason\":\"foreground proof: clear every pup window first\"}" >/dev/null 2>&1; done+try: print(' '.join(s.get('sessionId')+'('+str(s.get('owner'))+')' for s in json.load(sys.stdin).get('sessions') or [] if s.get('owner')!='pup-foreground-proof'))+except Exception: print('?unreadable')")+if [ -n "$OTHERS" ]; then echo "FOREGROUND PROOF: INVALID - other threads have pup windows on $BOX: $OTHERS. The gate restarts the browser and would close them; run it on a box with no other pup windows."; exit 2; fi+for W in $(printf '%s' "$SESS" | python3 -c "+import sys,json+try: print(' '.join(s.get('sessionId') for s in json.load(sys.stdin).get('sessions') or [] if s.get('owner')=='pup-foreground-proof'))+except Exception: pass"); do timeout 60 $AB pup_close_window "{\"window\":\"$W\",\"reason\":\"foreground proof: clear the gate's own leftover windows\"}" >/dev/null 2>&1; done cat > "$S/cold.ps1" <<'PS' Get-CimInstance Win32_Process -Filter "Name='chrome.exe'" | Where-Object { $_.CommandLine -match 'pup-profiles' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } Start-Sleep -Seconds 2; "cold"
tools/jumplist-ralph.py+99@@ -27,6 +27,13 @@ def b64(o): return base64.b64encode(json.dumps(o, separators=(',', ':')).encode( def main(): ap = argparse.ArgumentParser(); ap.add_argument('--target', required=True); a = ap.parse_args(); t = a.target+ # 2.0.700: this gate fires the real "Close ALL Adom Pup windows" task and closes every window between checks+ # (pup_close everyone), which takes other threads' windows with it. A box where another thread has windows is+ # not a valid box: refuse (INVALID, exit 2) and name them, as tools/foreground-proof.sh does.+ others = [f"{s.get('sessionId')}({s.get('owner')})" for s in (ab(t, 'pup_status', {}).get('sessions') or []) if s.get('owner') not in (THREAD, 'user-taskbar-menu')]+ if others:+ print(f"JUMPLIST RALPH: INVALID - other threads have pup windows on {t}: {' '.join(others)}. This gate closes every pup window; run it on a box with no other pup windows.")+ sys.exit(2) exe = ps(t, "Get-ChildItem (Join-Path $env:LOCALAPPDATA 'Adom Bridge\\bridges-cache\\puppeteer') -Filter *.exe | Select-Object -First 1 -ExpandProperty FullName", 'jump-list ralph: locate the pup exe') shim = ps(t, "Join-Path $env:LOCALAPPDATA 'Adom Bridge\\ab-taskrun.exe'", 'jump-list ralph: locate the taskrun shim') print(f'exe={exe}\nshim={shim}')@@ -48,8 +55,100 @@ def main(): lau = (ab(t, 'pup_list_windows', {}).get('sessions') or [{}]) lau = next((s.get('lastAgentUpdate') or {} for s in lau if s.get('sessionId') == sid), {}) check('annotate overlay armed', e.get('result') is True, f'({out}; lastAgentUpdate={lau.get("verb")})')+ # 2.0.444 (John: "'annotate this page' on the fusion taskbar icon did nothing"): a HUMAN caller's+ # annotate must bring the window to the screen, not arm a pen on a window parked at the bottom.+ wi = ab(t, 'pup_window_info', {'window': sid})+ fg = ab(t, 'desktop_get_foreground', {})+ fgh = fg.get('hwnd') or (fg.get('window') or {}).get('hwnd')+ idle = (ab(t, 'desktop_user_idle_ms', {}) or {}); idle_ms = idle.get('idleMs') if isinstance(idle.get('idleMs'), int) else (json.loads(idle.get('output') or '{}').get('idleMs') if isinstance(idle.get('output'), str) else None)+ raised_ok = bool(wi.get('foreground')) and (fgh is None or fgh == r.get('hwnd'))+ elevated = ''+ if not raised_ok:+ probe = ab(t, 'desktop_bring_to_front', {'hwnd': r.get('hwnd'), 'reason': 'jump-list ralph: read why Windows refused the Annotate raise'})+ if 'INTEGRITY' in (probe.get('error') or '').upper(): elevated = (json.loads(ab(t, 'desktop_get_foreground', {}).get('output') or '{}').get('title') or '?')+ if not raised_ok and elevated:+ print(f' SKIP annotate RAISED the window: the foreground belongs to an elevated process ({elevated!r}); Windows refuses a foreground switch away from it for every process, ab included')+ elif not raised_ok and isinstance(idle_ms, int) and idle_ms < 200_000:+ # Windows refuses a foreground switch while the user is active (foreground lock, ~200 s after input); a+ # scripted raise from shell_execute cannot win that, by design. The click path (shim from the jump list) can.+ print(f' SKIP annotate RAISED the window: someone used this box {idle_ms // 1000}s ago (foreground lock active); the raise refusal is Windows, not pup')+ else:+ check('annotate RAISED the window (user caller)', raised_ok, f'(session.foreground={wi.get("foreground")}, os fg hwnd={fgh}, window hwnd={r.get("hwnd")}, idleMs={idle_ms})') ab(t, 'pup_eval', {'window': sid, 'expr': 'window.__pupAnnotate && window.__pupAnnotate.exit && window.__pupAnnotate.exit(); true'}) + # 1b. The Adom TOOLBAR pen (2.0.453): worker.js sends only the tab URL and caller user-toolbar; pup must+ # resolve the window from the URL (John, 2026-09-10: after a wake the pen did nothing).+ print('\n== Toolbar pen by _tabUrl (worker.js payload)')+ tabs = ab(t, 'pup_list_tabs', {'window': sid}); cur = ((tabs.get('tabs') or [{}])[0].get('url')) or WIKI+ body = json.dumps({'command': 'pup_annotate', 'args': {'_tabUrl': cur, 'tool': 'pen', 'caller': {'aiThread': 'user-toolbar'}}})+ out = ps(t, "$pf = Join-Path $env:LOCALAPPDATA 'Adom Bridge\\pup-bridge-port.json'; $port = (Get-Content $pf -Raw | ConvertFrom-Json).port; (Invoke-WebRequest -Uri ('http://127.0.0.1:' + $port + '/command') -Method Post -ContentType application/json -Headers @{'x-adom-caller-thread'='user-toolbar'} -Body '" + body.replace("'", "''") + "' -UseBasicParsing).Content", 'jump-list ralph: the toolbar pen payload (_tabUrl, user-toolbar)')+ time.sleep(2); e = ab(t, 'pup_eval', {'window': sid, 'expr': '!!window.__pupAnnotate'})+ started = ('started\\":true' in str(out)) or ('"started":true' in str(out)) # the reply is an escaped JSON envelope+ check('toolbar pen resolved the window by _tabUrl and armed', e.get('result') is True and started, f'(url={cur[:60]}; started={started}; reply={str(out)[:400]})')+ # 1b2. Esc closes the overlay (2.0.457, John: "esc key should let me turn it back off"): a real key event.+ ab(t, 'pup_press_key', {'window': sid, 'key': 'Escape'}); time.sleep(1)+ e2 = ab(t, 'pup_eval', {'window': sid, 'expr': '!window.__pupAnnotate'})+ check('Esc closed the annotation overlay', e2.get('result') is True, f'(overlay gone={e2.get("result")})')+ ab(t, 'pup_eval', {'window': sid, 'expr': 'window.__pupAnnotate && window.__pupAnnotate.exit && window.__pupAnnotate.exit(); true'})+ # 1b3. Focus emulation follows the active tab (2.0.458, John: touchpad scrolling stuttering with 21 tabs).+ # Every attached tab used to carry it, so no background tab was ever timer-throttled or discardable.+ print('\n== Focus emulation is not left on every tab')+ ab(t, 'pup_open_tab', {'window': sid, 'url': WIKI + '/releases'}); time.sleep(3)+ rows = (ab(t, 'pup_list_tabs', {'window': sid}).get('tabs') or []) # list_tabs resolves the SESSION, so it does not perturb the state it reports+ emulated = [x.get('tabId') for x in rows if x.get('focusEmulated')]+ active = [x.get('tabId') for x in rows if x.get('active')]+ check('only the active tab is focus-emulated', len(rows) >= 2 and emulated == active, f'(tabs={len(rows)}, emulated={emulated}, active={active})')+ ab(t, 'pup_close', {'all': True, 'everyone': True}); time.sleep(1)+ r = ab(t, 'pup_open_window', {'window': 'jl-ralph', 'url': WIKI, 'owner': THREAD}, timeout=150); sid = r.get('sessionId') or 'jl-ralph'; time.sleep(6)++ # 1b4. A press must never be left down (2.0.460, John: "i still can't scroll with my touchpad on ANY of your+ # pup windows... i also can't change tabs"). A release that failed mid-click left Chrome treating every later+ # mouse event as one drag. The next mouse verb on the tab has to finish that click.+ print('\n== A stuck mouse button is released before the next mouse verb')+ ab(t, 'pup_eval', {'window': sid, 'expr': "window.__ups = 0; window.__downs = 0; addEventListener('pointerdown', () => window.__downs++, true); addEventListener('pointerup', () => window.__ups++, true); true"})+ ab(t, 'pup_input_dispatch', {'window': sid, 'type': 'mousePressed', 'x': 40, 'y': 300, 'button': 'left'}) # a press with no release, exactly what a timed-out release leaves behind+ d1 = ab(t, 'pup_eval', {'window': sid, 'expr': 'window.__downs + "/" + window.__ups'})+ ab(t, 'pup_click', {'window': sid, 'x': 60, 'y': 320}) # any later mouse verb must release it first+ d2 = ab(t, 'pup_eval', {'window': sid, 'expr': 'window.__downs + "/" + window.__ups'})+ ups = int(str(d2.get('result') or '0/0').split('/')[1]); downs = int(str(d2.get('result') or '0/0').split('/')[0])+ check('the orphaned press was released', ups >= downs and ups >= 2, f'(down/up after the orphan press: {d1.get("result")}, after the next click: {d2.get("result")})')++ # 1b5. Every open tells the calling AI what it is spending (2.0.462, John: "if the ai knew how much ram it+ # was using, that might have helped the ai decide... you should be automatically telling it what % of ram+ # was even left on the box").+ print('\n== Memory is reported back to the caller')+ rt = ab(t, 'pup_open_tab', {'window': sid, 'url': WIKI + '/releases'}); time.sleep(2)+ m = rt.get('memory') or {}+ ok_mem = isinstance(m.get('boxFreePct'), int) and isinstance(m.get('boxTotalMb'), int) and m.get('boxTotalMb') > 0 and isinstance(m.get('_hint'), str) and str(m.get('boxFreePct')) in m.get('_hint', '')+ check('open_tab reports box RAM and what a tab costs', ok_mem, f"(freePct={m.get('boxFreePct')}, browserMb={m.get('browserMb')}, avgMbPerTab={m.get('avgMbPerTab')}, tabs={m.get('tabsOnThisBrowser')})")+ lw = ab(t, 'pup_list_windows', {}).get('memory') or {}+ check('pup_list_windows reports it too', isinstance(lw.get('boxFreePct'), int), f"(freePct={lw.get('boxFreePct')})")++ # 1b6. An idle browser is released (2.0.463, John: "if pup has no windows open, why is it even using 1.2gb+ # across 8 processes?"). Set the grace to 1 min, close everything, and it must be gone within ~90 s.+ print('\n== An idle browser is released')+ ab(t, 'pup_configure', {'idleBrowserQuitMin': '1'})+ ab(t, 'pup_close', {'all': True, 'everyone': True}); time.sleep(2)+ gone = False+ for _ in range(10):+ time.sleep(15)+ st = ab(t, 'pup_status', {})+ m = st.get('memory') or {}+ if not m.get('browserMb'): gone = True; break+ check('the browser with no windows was closed', gone, '(pup_status no longer reports a browser for the profile)')+ ab(t, 'pup_configure', {'idleBrowserQuitMin': '10'})+ r = ab(t, 'pup_open_window', {'window': 'jl-ralph', 'url': WIKI, 'owner': THREAD}, timeout=150); sid = r.get('sessionId') or 'jl-ralph'; time.sleep(6)++ # 1c. A tab the USER opens in a pup window (window.open from the page, outside pup) is adopted by that+ # window's session within a health tick (2.0.453).+ print('\n== User-opened tab adoption')+ before = len(ab(t, 'pup_list_tabs', {'window': sid}).get('tabs') or [])+ ab(t, 'pup_eval', {'window': sid, 'expr': "window.open('https://wiki.adom.inc/adom/pup-bridge/releases', '_blank'); true"})+ time.sleep(3); after = ab(t, 'pup_list_tabs', {'window': sid}).get('tabs') or []+ check('user-opened tab adopted into the window session', len(after) == before + 1 and any('releases' in (x.get('url') or '') for x in after), f'(tabs {before} -> {len(after)})')+ ab(t, 'pup_close', {'all': True, 'everyone': True}); time.sleep(1)+ r = ab(t, 'pup_open_window', {'window': 'jl-ralph', 'url': WIKI, 'owner': THREAD}, timeout=150); sid = r.get('sessionId') or 'jl-ralph'; time.sleep(6)+ # 2. Switch to public view / logged-in view print('\n== Switch to public view'); out = fire('pup_wiki_set_view', {'sessionId': sid, 'view': 'public'}, 'jump-list ralph: public view task'); time.sleep(6) u = ab(t, 'pup_eval', {'window': sid, 'expr': 'location.href'}).get('result') or ''