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
New dev skill pup-hero-image: the full hero build recipe (canonical parameterized build script + fonts/logo/chrome/tile/badge assets), the Bridge-matched design constants, the hero-studio human-generate gate, and the traps; includes the vitrine-to-pill right-edge alignment John requested
7 files changed
+256
dev-skills/pup-hero-image/SKILL.md+91dev-skills/pup-hero-image/assets/familjen-700.ttfdev-skills/pup-hero-image/assets/satoshi-500.ttfdev-skills/pup-hero-image/assets/tiles/pup-cat-app9.icodev-skills/pup-hero-image/assets/tiles/pup-cat-default9.icodev-skills/pup-hero-image/assets/tiles/pup-cat-mixed9.icodev-skills/pup-hero-image/build_hero.py+165dev-skills/pup-hero-image/SKILL.mdadded+91@@ -0,0 +1,91 @@+---+name: pup-hero-image+description: DEV skill (source-only, never shipped in the pkg) - how to rebuild, tweak, and publish pup's wiki hero image. The full recipe: the parameterized build script, the design constants matched to Bridge's hero, the hero-studio human-generate gate, and every trap that bit us (gitignore whitelist, stale studio slugs, overlay defaults, other threads' studios).+---++# pup's hero image: build, tweak, publish++The pup wiki hero ([adom/pup-bridge](https://wiki.adom.inc/adom/pup-bridge)) is a fully **baked PNG**+built by `build_hero.py` in this folder, then run through hero-studio's human Generate gate for+provenance, then applied to the page. It is NOT drawn live by hero-studio: in full-hero mode the+studio contributes nothing but the provenance stamp.++## Design spec (matched to Bridge's hero, measured not guessed)++Canvas 2000x1250 (16:10), 1cqw = 20px.++| Element | Spec |+|---|---|+| Background | #0d1117 with a soft teal glow ellipse upper-right (blur 240) |+| ADOM logo | white, 11.8cqw wide, at (4.5cqw, 3.2cqw) |+| Title "Pup" | Familjen 700, **12cqw (240px em)** - same as Bridge's and kicad's heroes |+| Subtitle | Satoshi 500 50px, "A real browser **sandbox** you fully control." - sandbox in #4aa8ff |+| BRIDGE pill | solid teal #00cec2, dark text #091418, Familjen 700, top-right inset 2.6cqw (52px) |+| Vitrine | vy=140, VH=1050, width 1062, **right edge = pill right edge (W-52)**, rounded R=22, shadow blur 30 at +10/+16 |+| Vitrine chrome | real captured Chrome titlebar strip (`assets/win_slot3.png`) |+| Vitrine content | a FRESH `pup_screenshot_full_res` of a good wiki page, cover-fit, bottom 10% faded |+| Taskbar band | 68px, 5 badged pup tiles LEFT-aligned (no start orb), active tile underlined #78c8ff |+| Jump-list | Win11-style flyout above the active tile: RECENT (3 rows, first highlighted) + TASKS ("New pup window"), teal favicons |++The vitrine-pill right-edge alignment is deliberate (John 2026-08-09: "vitrine is a bit too far to+the right and doesn't quite line up with the pill"). If you move the pill inset, the vitrine+follows automatically (`vx = (W - pill_inset) - vw`).++## Rebuild++```bash+# 1. fresh content screenshot (do NOT reuse a stale one - the wiki UI changes)+adom-bridge-cli --target <box> --ai-thread "<you>" pup_open_window '{"sessionId":"hero-shot","profile":"hero-shot","url":"https://wiki.adom.inc/adom/alex-nameplate"}'+adom-bridge-cli --target <box> --ai-thread "<you>" pup_screenshot_full_res '{"sessionId":"hero-shot"}'++# 2. build+python3 dev-skills/pup-hero-image/build_hero.py --content <that.png> --out /tmp/hero_new.png+```++Assets (fonts, logo, chrome strip, taskbar tiles, badges) live in `assets/` right here, so the+build is reproducible from a bare clone. The fonts were converted from hero-studio's woff2.++## The human Generate gate (provenance)++The registry wants a provenance-stamped hero. The stamp comes ONLY from a human clicking+**Generate** in hero-studio - do not fake it, do not skip it.++```bash+HERO_STUDIO_PORT=<free port> hero-studio # START YOUR OWN - never take another thread's studio+HERO_STUDIO_PORT=<port> hero-studio vitrine --file /tmp/hero_new.png --full --chrome none+HERO_STUDIO_PORT=<port> hero-studio set slug=pup-bridge page=adom/pup-bridge overlay.pill=false overlay.slug=false overlay.author=false+```++Open `https://<container-slug>.adom.cloud/proxy/<port>/` in a pup tab for John, flash the taskbar+(never foreground), and let him review + click Generate. Then fetch+`http://localhost:<port>/generated/<file from /state _gen>` - that PNG is the final.++## Apply to the live page++```bash+cp <generated.png> hero-v9.png+adom-wiki repo push --files hero-v9.png -m "<real changelog>" adom/pup-bridge+```++Verify live bytes match (`adom-wiki page hero adom/pup-bridge --json` reports bytes), then refresh+the live page in a pup window with a cache-bust (`?v=<something new>`) and flash - standing rule,+never just say "published".++## Traps (every one of these bit us)++- **`.gitignore` whitelists heroes**: `*.png` is ignored; only `!hero-vN.png` entries push. A new+ filename needs its own whitelist line FIRST or `repo push` silently skips it (commit: null).+- **`adom-wiki page hero --image` is broken** (looks for screenshots/hero.png) - overwrite the+ tracked whitelisted hero file instead. That's why the file is still named `hero-v9.png`.+- **hero-studio defaults `overlay.slug=true`** on a fresh port - turn off pill AND slug AND author+ or the export gets a baked slug in the dynamic marquee band (hero-studio#29).+- **The studio package dropdown may list pup under the OLD slug** (`adom/adom-desktop-puppeteer-bridge`,+ hero-studio issue: renamed pages keep stale option values). If `set page=` won't bind, select the+ option in the DOM via pup_eval. The EXPORT slug follows `set slug=pup-bridge` regardless.+- **Never take over another thread's hero-studio port** - John's other build threads run their own+ studios. `hero-studio state` on an existing port tells you whose it is; start a fresh port.+- **Generated filename may carry the old slug** (from the dropdown's stale value) - harmless, the+ image is what ships, but check the composed image, not the filename.+- The full build history and the measured Bridge-hero constants came from the 2026-08-08/09 session;+ if Bridge's hero is redesigned, re-measure (title glyph block, logo width, pill inset) before+ changing the constants here.
dev-skills/pup-hero-image/assets/familjen-700.ttfadded⋯ 1 unchanged line ⋯
dev-skills/pup-hero-image/assets/satoshi-500.ttfadded⋯ 1 unchanged line ⋯
dev-skills/pup-hero-image/assets/tiles/pup-cat-app9.icoadded⋯ 1 unchanged line ⋯
dev-skills/pup-hero-image/assets/tiles/pup-cat-default9.icoadded⋯ 1 unchanged line ⋯
dev-skills/pup-hero-image/assets/tiles/pup-cat-mixed9.icoadded⋯ 1 unchanged line ⋯
dev-skills/pup-hero-image/build_hero.pyadded+165@@ -0,0 +1,165 @@+#!/usr/bin/env python3+"""Canonical pup hero build (dev-skills/pup-hero-image).++Rebuilds the FULL pup wiki hero from assets: background, ADOM logo, "Pup" title,+sandbox subtitle, solid BRIDGE pill, the vitrine (a real pup Chrome window showing a+wiki page, taskbar band with badged pup tiles, bottom fade), the Windows-11 jump-list+flyout, drop shadow, rounded corners. Output: hero.png at 2000x1250 (16:10).++Usage:+ python3 build_hero.py --content <screenshot.png> [--out hero.png]++--content is the page screenshot shown INSIDE the vitrine: take a FRESH one with+pup_screenshot_full_res of a good-looking wiki page (the shipped hero used the Alex+LED-nameplate hardware page). Do NOT reuse a stale screenshot of an old UI.++Design constants are matched to Bridge's hero (adom/adom-bridge) — measured, not+guessed. If Bridge's hero changes, re-measure before touching these.+"""+import argparse, os+from PIL import Image, ImageDraw, ImageFilter, ImageFont++HERE = os.path.dirname(os.path.abspath(__file__))+A = lambda *p: os.path.join(HERE, 'assets', *p)++ap = argparse.ArgumentParser()+ap.add_argument('--content', required=True, help='page screenshot for the vitrine body')+ap.add_argument('--out', default='hero.png')+args = ap.parse_args()++def load(f): return Image.open(f).convert('RGBA')++W, H = 2000, 1250+CQ = W / 100.0 # 1cqw = 20px on this canvas++# ── background: near-black with a soft teal glow upper-right ─────────────────+bg = Image.new('RGB', (W, H), (13, 17, 23))+glow = Image.new('L', (W, H), 0)+ImageDraw.Draw(glow).ellipse([W*0.45, -H*0.3, W*1.25, H*0.9], fill=90)+glow = glow.filter(ImageFilter.GaussianBlur(240))+bg = Image.composite(Image.new('RGB', (W, H), (18, 52, 58)), bg, glow).convert('RGBA')++# ── vitrine geometry ─────────────────────────────────────────────────────────+# Right edge ALIGNS with the BRIDGE pill's right edge (both inset 2.6cqw = 52px).+# John 2026-08-09: "vitrine is a bit too far to the right, doesn't line up with the pill".+vy = 140+vw = 1062+vx = (W - int(2.6*CQ)) - vw # 886: right edge at W-52 = pill right edge+VH = 1050+TB = 68 # taskbar band height+win_h = VH - TB++# ── window chrome strip (real captured Chrome titlebar) ──────────────────────+chrome = Image.open(A('win_slot3.png')).convert('RGB'); CH_SRC = 125+chrome = chrome.crop((0, 0, chrome.width, CH_SRC)).resize((vw, int(CH_SRC*vw/1116)), Image.LANCZOS)+CH = chrome.height++# ── page content, cover-fit, bottom 10% fade ─────────────────────────────────+content = Image.open(args.content).convert('RGB'); CONT_H = win_h - CH+s = max(vw/content.width, CONT_H/content.height)+content = content.resize((int(content.width*s), int(content.height*s)), Image.LANCZOS)+cx = (content.width - vw)//2+content = content.crop((cx, 0, cx+vw, CONT_H))+panel = Image.new('RGBA', (vw, VH), (24, 26, 31, 255))+panel.paste(chrome.convert('RGBA'), (0, 0))+panel.paste(content.convert('RGBA'), (0, CH))+fade_h = int(CONT_H*0.10)+alpha = Image.linear_gradient('L').resize((vw, fade_h))+solid = Image.new('RGBA', (vw, fade_h), (24, 26, 31, 255)); solid.putalpha(alpha)+panel.alpha_composite(solid, (0, CH+CONT_H-fade_h))++# ── taskbar band: 5 badged pup tiles, LEFT-aligned, no start orb ─────────────+dp = ImageDraw.Draw(panel); tb_y = VH - TB+dp.rectangle([0, tb_y, vw, VH], fill=(24, 26, 31, 255))+dp.line([0, tb_y, vw, tb_y], fill=(44, 50, 60, 255))+tiles = [load(A('tiles', f'pup-cat-{k}9.ico')) for k in ('default', 'mixed', 'app', 'default', 'mixed')]+badges = ['sample-shotlog', 'domains-wiki', 'sample-charger', 'pup-hero', 'gwsrc-draft']+def button(base, bp, size=110):+ b = base.resize((size, size), Image.LANCZOS).copy()+ ov = load(A('badges', f'{bp}.png')).resize((int(size*0.52),)*2, Image.LANCZOS)+ b.alpha_composite(ov, (size-ov.width-2, size-ov.height-2)); return b+bh, gap, x = 54, 14, 14+icon_lefts = []+for i in range(5):+ bt = button(tiles[i], badges[i]).resize((bh, bh), Image.LANCZOS)+ by = tb_y + (TB-bh)//2+ panel.alpha_composite(bt, (x, by)); icon_lefts.append(x)+ if i == 0: dp.line([x+5, VH-4, x+bh-5, VH-4], fill=(120, 200, 255), width=4) # active underline+ x += bh + gap++# ── jump-list flyout above the ACTIVE (first) icon ───────────────────────────+jl = ImageDraw.Draw(panel)+f_item = ImageFont.truetype(A('satoshi-500.ttf'), 23)+f_hdr = ImageFont.truetype(A('satoshi-500.ttf'), 15)+recent = ["Pup - Puppeteer Bridge", "Adom Bridge", "Alex - Adom Molecule LED Nameplate"]+tasks = ["New pup window"]+pw_, pad, row_h, top_pad, hdr_h, div_gap, bot_pad, r = 366, 16, 46, 12, 30, 22, 12, 12+ph_ = top_pad + hdr_h + row_h*len(recent) + div_gap + hdr_h + row_h*len(tasks) + bot_pad+pl = max(6, icon_lefts[0] - 2) # panel-local x+pb = tb_y - 10; pt = pb - ph_+# shadow+psh = Image.new('RGBA', panel.size, (0, 0, 0, 0))+ImageDraw.Draw(psh).rounded_rectangle([pl, pt, pl+pw_, pb], radius=r, fill=(0, 0, 0, 150))+panel.alpha_composite(psh.filter(ImageFilter.GaussianBlur(18)))+jl.rounded_rectangle([pl, pt, pl+pw_, pb], radius=r, fill=(41, 41, 44, 252), outline=(78, 78, 84, 255), width=1)+def favicon(cx_, cy_, s_=26):+ x0, y0 = cx_-s_//2, cy_-s_//2+ jl.rounded_rectangle([x0, y0, x0+s_, y0+s_], radius=6, fill=(0, 206, 194, 255))+ gr = s_//2-6+ jl.ellipse([cx_-gr, cy_-gr, cx_+gr, cy_+gr], outline=(9, 26, 28, 255), width=2)+ jl.line([cx_-gr, cy_, cx_+gr, cy_], fill=(9, 26, 28, 255), width=1)+ jl.line([cx_, cy_-gr, cx_, cy_+gr], fill=(9, 26, 28, 255), width=1)+def trunc(t, f, mw):+ if jl.textlength(t, font=f) <= mw: return t+ while t and jl.textlength(t+"…", font=f) > mw: t = t[:-1]+ return t+"…"+y = pt + top_pad+def section(t):+ global y; jl.text((pl+pad, y+6), t.upper(), font=f_hdr, fill=(150, 150, 156, 255)); y += hdr_h+def item(t, hl=False):+ global y+ if hl: jl.rounded_rectangle([pl+6, y+3, pl+pw_-6, y+row_h-3], radius=7, fill=(58, 58, 64, 255))+ favicon(pl+pad+13, y+row_h//2)+ tx = pl+pad+40+ jl.text((tx, y+row_h//2-13), trunc(t, f_item, pl+pw_-pad-tx), font=f_item, fill=(236, 236, 239, 255))+ y += row_h+section("Recent")+for i, t in enumerate(recent): item(t, hl=(i == 0))+y += div_gap//2; jl.line([pl+pad, y, pl+pw_-pad, y], fill=(70, 70, 76, 255), width=1); y += div_gap//2+section("Tasks")+for t in tasks: item(t)++# ── composite vitrine: rounded corners + drop shadow ─────────────────────────+R = 22+mask = Image.new('L', (vw, VH), 0); ImageDraw.Draw(mask).rounded_rectangle([0, 0, vw, VH], R, fill=255)+sh = Image.new('RGBA', (W, H), (0, 0, 0, 0)); shm = Image.new('L', (W, H), 0)+ImageDraw.Draw(shm).rounded_rectangle([vx+10, vy+16, vx+vw+10, vy+VH+16], R, fill=150)+shm = shm.filter(ImageFilter.GaussianBlur(30)); sh.putalpha(shm)+bg = Image.alpha_composite(bg, sh); bg.paste(panel, (vx, vy), mask)++# ── left content: logo, title, subtitle (matched to Bridge's hero) ───────────+d = ImageDraw.Draw(bg); inset = int(4.5*CQ)+logo = Image.open(A('adom_logo_white.png')).convert('RGBA'); lw = int(11.8*CQ)+logo = logo.resize((lw, int(logo.height*lw/logo.width)), Image.LANCZOS)+bg.alpha_composite(logo, (inset, int(3.2*CQ)))+f_title = ImageFont.truetype(A('familjen-700.ttf'), int(12*CQ)) # 12cqw = 240px em+bbt = d.textbbox((0, 0), "Pup", font=f_title); ty = int(13.15*CQ)+d.text((inset-bbt[0], ty-bbt[1]), "Pup", font=f_title, fill=(230, 237, 243, 255))+f_sub = ImageFont.truetype(A('satoshi-500.ttf'), 50)+BLU, WHT = (74, 168, 255, 255), (232, 238, 244, 255)+sy = ty + (bbt[3]-bbt[1]) + int(2.6*CQ)+d.text((inset, sy), "A real browser ", font=f_sub, fill=WHT)+w1 = d.textlength("A real browser ", font=f_sub)+d.text((inset+w1, sy), "sandbox", font=f_sub, fill=BLU)+d.text((inset, sy+int(50*1.35)), "you fully control.", font=f_sub, fill=WHT)++# ── BRIDGE pill: solid teal, top-right, inset 2.6cqw ─────────────────────────+pf = ImageFont.truetype(A('familjen-700.ttf'), 26); txt = "BRIDGE"+tb2 = d.textbbox((0, 0), txt, font=pf); tw, th = tb2[2]-tb2[0], tb2[3]-tb2[1]+padx, pady = 22, 12; pw2, ph2 = tw+padx*2, th+pady*2+px_ = W - int(2.6*CQ) - pw2; py_ = int(2.6*CQ)+d.rounded_rectangle([px_, py_, px_+pw2, py_+ph2], radius=ph2//2, fill=(0, 206, 194, 255))+d.text((px_+padx-tb2[0], py_+pady-tb2[1]), txt, font=pf, fill=(9, 20, 24, 255))++bg.convert('RGB').save(args.out)+print(f"saved {args.out} vitrine right edge={vx+vw} pill right edge={W-int(2.6*CQ)} (aligned)")