master
John Lauer Update 5 file(s) 98d2966 2mo ago
#!/usr/bin/env python3
"""The pup demo tour (John, 2026-09-05): eight beats, driven at the user's pace through ab's demo kit
(transport panel + captions + the Adom cursor), recorded with pup's own screen recorder, on a desktop
the AI may take over. Modeled on kicad's demo: every beat narrates, points, and leaves a screenshot.

  tools/pup-demo.py --target arav-rog [--no-record] [--no-panel] [--beats 1,2,3]

Beats: 1 the pup window · 2 taskbar identity (tile, favicon badge, progress bar) · 3 jump lists ·
4 the toolbar, annotate, and the adom-you profile · 5 automated Google login (to the 2FA wall) ·
6 screenshots for the AI · 7 UltraLibrarian login · 8 close-all from the jump list.
Rules from the demo skills: foreground ONCE then yield; one big centred window (98% of the work
area); cursor hidden after the last prelude click; never pace with sleeps the user cannot pause.
"""
import argparse, json, subprocess, sys, time

THREAD = "pup demo"
WIKI = "https://wiki.adom.inc/adom/pup-bridge"
GOOGLE_USER = "[email protected]"
STOPS = ["The pup window", "Taskbar identity", "Jump lists", "Toolbar, annotate, adom-you", "Google login", "Screenshots for the AI", "UltraLibrarian login", "Close all from the jump list"]

class Demo:
    def __init__(self, target, record=True, panel=True):
        self.t, self.record, self.panel = target, record, panel
        self.rec_id = None; self.hwnd = None; self.stage = 0; self.shots = []
    def ab(self, verb, args, timeout=120):
        p = subprocess.run(['adom-bridge', '--target', self.t, '--ai-thread', THREAD, verb, json.dumps(args)], capture_output=True, text=True, timeout=timeout + 30)
        try: d = json.loads(p.stdout)
        except Exception: return {'error': (p.stdout + p.stderr)[-200:]}
        o = d.get('output'); inner = json.loads(o) if isinstance(o, str) and o.startswith('{') else {}
        return {**inner, **d}
    def cap(self, text, ms=6000):
        print(f'  ▶ {text}'); self.ab('desktop_caption', {'id': 'pup-demo', 'text': text, 'duration': ms, 'position': 'bottom', 'size': 'large'})
    def say(self, text, ms=6000, hold=None):
        self.cap(text, ms); self.wait(hold if hold is not None else min(ms / 1000, 5))
    def wait(self, sec):
        # pace through the panel: a paused user holds us; a stop ends the show
        end = time.time() + sec
        while time.time() < end:
            time.sleep(min(2, max(0.2, end - time.time())))
            if self.panel: self.poll()
    def poll(self):
        r = self.ab('desktop_demo_panel', {'action': 'poll', 'id': 'pup-tour'})
        ev = r.get('event') or (r.get('events') or [None])[-1]
        if ev == 'stop': raise SystemExit('user stopped the tour')
        while r.get('state') == 'paused':
            time.sleep(2); r = self.ab('desktop_demo_panel', {'action': 'poll', 'id': 'pup-tour'})
            if (r.get('event') or '') == 'stop': raise SystemExit('user stopped the tour')
    def stop(self, n):
        self.stage = n; print(f'\n=== {n+1}. {STOPS[n]}')
        if self.panel: self.ab('desktop_demo_panel', {'action': 'show', 'id': 'pup-tour', 'title': 'pup tour', 'stops': STOPS, 'stage': n, 'state': 'playing', 'reason': 'Guided tour of pup, driven at your pace.'})
    def shot(self, label, **kw):
        r = self.ab('pup_screenshot', {'window': 'pup-demo', **kw}); fp = r.get('filePath')
        if fp: self.shots.append({'label': label, 'path': fp}); print(f'  📷 {label}: {fp}')
        return r

    # ── recording ──
    def rec_start(self):
        if not self.record: return
        self.ab('pup_recorder_open', {'reason': 'Recording the pup demo tour.'}); time.sleep(4)
        r = self.ab('pup_screen_record_start', {'confirmDesktopNotTabRecording': True, 'reason': 'Recording the pup demo tour.', 'fps': 30, 'maxDurationMs': 15 * 60 * 1000})
        self.rec_id = r.get('recordingId'); print(f'  ● recording {self.rec_id} -> {r.get("filePath")}')
    def rec_stop(self):
        if not self.rec_id: return None
        r = self.ab('pup_screen_record_stop', {}); self.ab('pup_recorder_close', {})
        print(f'  ■ recording saved: {r.get("filePath")} ({r.get("sizeKB")} KB)'); return r.get('filePath')

    # ── beats ──
    def beat1_window(self):
        self.stop(0)
        self.cap('This is a pup window: a real Chrome for Testing, in a profile pup owns, driven by the AI over CDP. It opens in the BACKGROUND: nothing steals your screen, the taskbar button just appears.', 9000)
        r = self.ab('pup_open_window', {'window': 'pup-demo', 'url': WIKI, 'owner': THREAD}, timeout=150)
        self.hwnd = r.get('hwnd'); print(f'  window hwnd={self.hwnd} browser={ (r.get("browser") or {}).get("kind") }')
        self.wait(3)
        # Bring it up the way a user does: ONE real click on its taskbar button. That click is also the
        # only thing that clears the Windows 11 attention tint a background-born button wears (a tinted
        # button hides its progress bar; measured 2026-09-05). Fallback: pup's own raise.
        self.cap('Click its taskbar button and it comes up. That is the only time this tour takes the front.', 5000)
        rect = self.taskbar_button()
        if rect:
            self.ab('desktop_click', {'space': 'screen', 'x': rect.get('centerX', rect['x'] + rect['w'] // 2), 'y': rect.get('centerY', rect['y'] + rect['h'] // 2), 'reason': 'the pup demo tour: bring the demo window up by its taskbar button, as a user would'})
        else:
            self.ab('pup_raise_os_window', {'window': 'pup-demo', 'foregroundReason': 'the pup demo tour: the user asked to watch pup on this screen'})
        self.wait(1.5)
        # one big centred window: 98% of the primary work area
        mons = self.ab('desktop_list_monitors', {}); ms = mons.get('monitors') or []
        wa = next((m.get('workArea') for m in ms if m.get('primary')), None) or (ms[0].get('workArea') if ms else None)
        if wa and self.hwnd:
            w, h = int(wa['width'] * 0.98), int(wa['height'] * 0.98); x, y = wa['x'] + (wa['width'] - w) // 2, wa['y'] + (wa['height'] - h) // 2
            self.ab('desktop_set_window_bounds', {'hwnd': self.hwnd, 'x': x, 'y': y, 'w': w, 'h': h})
        self.park_mouse()
        self.wait(3); self.shot('window')
        self.say('Notice the toolbar: pup ships small extensions that ride every launch, and the owner chip bottom-left names the AI thread that owns this window.', 8000)

    def beat2_taskbar(self):
        self.stop(1)
        self.say('Look at the taskbar. That button is not "Google Chrome for Testing": pup stamps it with the AI thread name on the Adom tile, plus the page favicon as a badge. Every thread gets its own button.', 9000)
        # pup paints 30% / 75% / marquee / off through ab's ITaskbarList3. Measured 2026-09-05: Windows 11 25H2
        # (build 26200) on arav-rog renders NO taskbar progress for ANY app (plain COM on Notepad included), so
        # the caption states the capability without promising the pixels; on a box that renders it, it shows.
        self.cap('A long task can paint a progress bar on that button, so you see the work without opening the window. Some Windows builds no longer draw taskbar progress; the verb still runs.', 7000)
        self.ab('pup_progress', {'window': 'pup-demo', 'mode': 'value', 'value': 30, 'note': 'demo: 3 of 10 pages'}); self.wait(2.5)
        self.ab('pup_progress', {'window': 'pup-demo', 'mode': 'value', 'value': 75, 'note': 'demo: 8 of 10 pages'}); self.wait(2.5)
        self.ab('pup_progress', {'window': 'pup-demo', 'mode': 'indeterminate', 'note': 'demo: pulsing'}); self.wait(2.5)
        self.ab('pup_progress', {'window': 'pup-demo', 'mode': 'off'}); self.wait(1)

    def hud_hwnd(self):
        """The pup recorder HUD window (title 'Adom Bridge Recorder'), or None."""
        r = self.ab('desktop_list_windows', {}); ws = r.get('windows') or (r.get('data') or {}).get('windows') or []
        for w in ws:
            if 'Adom Bridge Recorder' in str(w.get('title') or ''): return w.get('hwnd')
        return None

    def tray_hwnd(self):
        if getattr(self, '_tray', None): return self._tray
        ps = 'Add-Type -Namespace W -Name U -MemberDefinition \'[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern IntPtr FindWindow(string c, string t);\'\n$h = [W.U]::FindWindow("Shell_TrayWnd", $null); "TRAY_HWND=" + [int64]$h\n'
        self.ab('desktop_write_file', {'path': 'C:/Users/Public/pup-tray.ps1', 'content': ps, 'reason': 'find the taskbar hwnd for the demo tour'})
        r = self.ab('shell_execute', {'command': 'powershell -NoProfile -ExecutionPolicy Bypass -File C:/Users/Public/pup-tray.ps1', 'timeoutSeconds': 30, 'reason': 'find the taskbar hwnd for the demo tour'})
        import re; m = re.search(r'TRAY_HWND=(\d+)', r.get('stdout') or ''); self._tray = int(m.group(1)) if m else None; return self._tray

    def taskbar_button(self):
        """The pup window's taskbar button rect: the tile named after the thread, else the stock CfT button."""
        tray = self.tray_hwnd()
        if not tray: return None
        t = self.ab('desktop_ui_tree', {'hwnd': tray, 'maxDepth': 6}); found = {}
        def walk(n):
            if isinstance(n, dict):
                nm = str(n.get('name') or ''); rect = n.get('rect') or n.get('bounds')
                if (n.get('role') or n.get('controlType')) == 'Button' and rect:
                    if nm.lower().startswith(THREAD.lower() + ' - '): found.setdefault('tile', rect)
                    elif nm.startswith('Google Chrome for Testing - '): found.setdefault('cft', rect)
                for c in (n.get('children') or []): walk(c)
            elif isinstance(n, list):
                for c in n: walk(c)
        walk(t.get('root') or t.get('tree') or t)
        rect = found.get('tile') or found.get('cft'); print(f'  taskbar button: {"tile" if found.get("tile") else ("cft" if rect else "none")} {rect}'); return rect

    def screen(self, label):
        r = self.ab('desktop_screenshot_screen', {}); fp = r.get('localSafePath') or r.get('path') or r.get('filePath')
        if fp: self.shots.append({'label': label, 'path': fp, 'screen': True}); print(f'  🖥 {label}: {fp}')
        return fp

    def beat3_jumplists(self):
        self.stop(2)
        self.say('Right-click that taskbar button and pup gives you a jump list: switch a wiki page between logged-in and public, annotate the page, open the dashboard, or close every pup window. Go ahead and right-click it now if you like; every task in it is a pup verb, which is what the last beat uses.', 11000)
        self.screen('taskbar-tile')

    def beat4_toolbar_annotate(self):
        self.stop(3)
        self.say('The pen in the toolbar arms annotate: draw on any page, and the moment you finish the marked-up region is captured, copied to your clipboard, and sent to the owning AI.', 8000)
        self.ab('pup_annotate', {'window': 'pup-demo', 'tool': 'pen'}); self.wait(2)
        pts = [(420, 320), (520, 330), (640, 360), (760, 350), (880, 330)]
        self.ab('pup_input_dispatch', {'window': 'pup-demo', 'type': 'mousePressed', 'x': pts[0][0], 'y': pts[0][1], 'button': 'left'})
        for x, y in pts[1:]: self.ab('pup_input_dispatch', {'window': 'pup-demo', 'type': 'mouseMoved', 'x': x, 'y': y, 'button': 'left'}); time.sleep(0.12)
        self.ab('pup_input_dispatch', {'window': 'pup-demo', 'type': 'mouseReleased', 'x': pts[-1][0], 'y': pts[-1][1], 'button': 'left'})
        self.wait(4); self.shot('annotate')
        self.say('And the identity tile: every pup window surfs as adom-you, ONE durable profile shared by every window. Each login it makes grows a vault in the OS keychain, so over time pup can sign you into everything it has ever seen.', 10000)

    def beat5_google(self):
        self.stop(4)
        self.say('Automated Google login. The credential came from the vault, imported from your own browser and synced from your laptop; pup types nothing you have to give it.', 8000)
        self.ab('pup_navigate', {'window': 'pup-demo', 'url': 'https://accounts.google.com/signin/v2/identifier'}); self.wait(4)
        self.ab('pup_click', {'window': 'pup-demo', 'selector': '#identifierId'}); self.ab('pup_type', {'window': 'pup-demo', 'text': GOOGLE_USER})
        self.ab('pup_click', {'window': 'pup-demo', 'selector': '#identifierNext'}); self.wait(5)
        r = self.ab('pup_login', {'window': 'pup-demo', 'submit': True, 'preferAccount': GOOGLE_USER, 'submitReason': 'accounts.google.com is Google itself; the demo signs into the user own Google account from the adom-you vault'}, timeout=90)
        self.wait(5); self.shot('google')
        if r.get('errorCode') == 'blocked_on_user':
            self.say(f'pup filled and submitted the password, then Google asked to verify it is really you ({r.get("wall")}). pup detects that wall and hands you the wheel instead of pretending. A real login, honestly reported.', 9000)
        else:
            self.say('Signed in. No dropdown, no foregrounding, and the domain was confirmed before the secret went out.', 6000)

    def beat6_screenshots(self):
        self.stop(5)
        self.ab('pup_navigate', {'window': 'pup-demo', 'url': WIKI}); self.wait(4)
        self.say('Screenshots for the AI. This is how the AI reads what you see: a full window capture, resized to the size a vision model reads best, saved on this machine.', 7000)
        self.shot('page')
        r = self.ab('pup_screenshot_element', {'window': 'pup-demo', 'selector': 'h1'})
        if r.get('filePath'): self.shots.append({'label': 'element', 'path': r['filePath']})
        self.say('Or just one element, cropped tight, so the AI spends its attention on the thing that matters.', 6000)

    def beat7_ultralibrarian(self):
        self.stop(6)
        self.say('UltraLibrarian, the CAD library vendor. Same vault, a real sign-in: pup fills, confirms the live domain, submits.', 7000)
        self.ab('pup_navigate', {'window': 'pup-demo', 'url': 'https://app.ultralibrarian.com/Account/Login?returnUrl=%252fsearch'}); self.wait(6)
        r = self.ab('pup_login', {'window': 'pup-demo', 'submit': True, 'submitReason': 'sso.ultralibrarian.com is the UltraLibrarian sign-in the user imported a login for; the demo signs in from the adom-you vault'}, timeout=90)
        self.wait(7); self.shot('ultralibrarian')
        who = self.ab('pup_eval', {'window': 'pup-demo', 'expr': '(document.body.innerText.match(/Hello [^\\n]+/)||[""])[0]'}).get('result') or ''
        self.say(f'Signed in{": " + who if who else ""}. That is a vendor portal pup can now use for you: search, download a part, all as you.', 7000)

    def beat8_close_all(self):
        self.stop(7)
        self.say('Last: the jump list task "Close ALL Adom Pup windows". One click there, or the same verb from an AI, and every pup window on this desktop is gone. Nothing left on your desktop.', 7000)
        self.ab('pup_close', {'all': True, 'everyone': True}); self.wait(3); self.screen('after-close-all')
        # The recorder HUD (a CfT window pup keeps alive for the take) becomes visible once the demo window
        # is gone, bottom-right, exactly where the bottom caption sits (take 2 review, 2026-09-05). Close the
        # HUD's cover first: minimize it for the closing line, then the caption goes top so nothing overlaps.
        hud = self.hud_hwnd()
        if hud: self.ab('desktop_set_window_state', {'hwnd': hud, 'state': 'minimize', 'reason': 'the pup demo tour: keep the recorder HUD out of the closing shot'})
        print(f'  ▶ closing line'); self.ab('desktop_caption', {'id': 'pup-demo', 'text': 'That is pup. Every window, tab, login and screenshot you saw was the AI driving a browser as you, without ever taking your desktop away.', 'duration': 8000, 'position': 'top', 'size': 'large'}); self.wait(5)

    def park_mouse(self):
        """Put the REAL mouse at the right screen edge (outside the 98% window) and hide ab's glow cursor.
        A take showed the arrow parked on the taskbar button for a full minute (demo-review, 2026-09-05)."""
        mons = self.ab('desktop_list_monitors', {}); ms = mons.get('monitors') or []
        m = next((m for m in ms if m.get('primary')), ms[0] if ms else None)
        if m:
            b = m.get('bounds') or m.get('rect') or m; x = int(b.get('x', 0) + b.get('width', b.get('w', 2560)) - 2); y = int(b.get('y', 0) + b.get('height', b.get('h', 1440)) // 2)
            self.ab('desktop_hover', {'space': 'screen', 'x': x, 'y': y, 'reason': 'park the mouse at the screen edge so it never sits on what the tour shows'})
        self.ab('desktop_cursor', {'action': 'hide'})

    def pre_take(self):
        """A clean desktop before the recorder starts: no leftover pup windows or idle Chromes (their
        taskbar buttons sit in the Windows 11 attention state, which hides the progress bar)."""
        self.ab('pup_close', {'all': True, 'everyone': True}); self.ab('pup_quit_idle_browsers', {}); time.sleep(2)
        self.park_mouse()

    def run(self, beats):
        self.pre_take()
        self.rec_start()
        try:
            for n in beats: getattr(self, ['beat1_window','beat2_taskbar','beat3_jumplists','beat4_toolbar_annotate','beat5_google','beat6_screenshots','beat7_ultralibrarian','beat8_close_all'][n])()
        finally:
            if self.panel: self.ab('desktop_demo_panel', {'action': 'hide', 'id': 'pup-tour'})
            self.ab('pup_close', {'all': True, 'everyone': True}); self.ab('desktop_cursor', {'action': 'hide'})
            video = self.rec_stop()
            print('\nSHOTS:', json.dumps(self.shots)); print('VIDEO:', video)

if __name__ == '__main__':
    ap = argparse.ArgumentParser(); ap.add_argument('--target', required=True); ap.add_argument('--no-record', action='store_true'); ap.add_argument('--no-panel', action='store_true'); ap.add_argument('--beats', default='0,1,2,3,4,5,6,7')
    a = ap.parse_args(); Demo(a.target, record=not a.no_record, panel=not a.no_panel).run([int(x) for x in a.beats.split(',')])