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
v2.0.688 (insiders): Smoother taskbar tile text: every tile size is drawn at 4x and averaged down, so letters sit at exact sub-pixel positions (John, 2026-10-03). Every Settings example image shows again: the sample badges and the badge-size, on/off and grouping previews were broken images because the page asked with n= and the routes read name=. pup_uninstall and pup_reap_profiles describe what the Rust exe does. The vault's card test no longer spells whole test card numbers (the wiki flagged the page as sensitive).
6 files changed
+76−30
rust/Cargo.lock+12−12@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-pup-bridge"-version = "2.0.686"+version = "2.0.688" dependencies = [ "pup-browser", "pup-cdp",@@ -655,7 +655,7 @@ dependencies = [ [[package]] name = "pup-browser"-version = "2.0.686"+version = "2.0.688" dependencies = [ "pup-core", "pup-host",@@ -667,7 +667,7 @@ dependencies = [ [[package]] name = "pup-cdp"-version = "2.0.686"+version = "2.0.688" dependencies = [ "serde_json", "thiserror",@@ -677,7 +677,7 @@ dependencies = [ [[package]] name = "pup-core"-version = "2.0.686"+version = "2.0.688" dependencies = [ "serde", "serde_json",@@ -687,7 +687,7 @@ dependencies = [ [[package]] name = "pup-dashboard"-version = "2.0.686"+version = "2.0.688" dependencies = [ "pup-cdp", "pup-core",@@ -700,7 +700,7 @@ dependencies = [ [[package]] name = "pup-host"-version = "2.0.686"+version = "2.0.688" dependencies = [ "serde", "serde_json",@@ -709,14 +709,14 @@ dependencies = [ [[package]] name = "pup-host-mac"-version = "2.0.686"+version = "2.0.688" dependencies = [ "pup-host", ] [[package]] name = "pup-host-win"-version = "2.0.686"+version = "2.0.688" dependencies = [ "pup-core", "pup-host",@@ -726,7 +726,7 @@ dependencies = [ [[package]] name = "pup-identity"-version = "2.0.686"+version = "2.0.688" dependencies = [ "fontdue", "image",@@ -736,7 +736,7 @@ dependencies = [ [[package]] name = "pup-session"-version = "2.0.686"+version = "2.0.688" dependencies = [ "pup-core", "serde",@@ -746,7 +746,7 @@ dependencies = [ [[package]] name = "pup-vault"-version = "2.0.686"+version = "2.0.688" dependencies = [ "pup-core", "pup-host",@@ -757,7 +757,7 @@ dependencies = [ [[package]] name = "pup-verbs"-version = "2.0.686"+version = "2.0.688" dependencies = [ "base64", "png 0.17.16",
rust/Cargo.toml+1−1@@ -4,7 +4,7 @@ members = ["core", "host", "host-win", "host-mac", "cdp", "browser", "session", # One version for the whole bridge, in lockstep with src/BRIDGE_VERSION (tools/ship.sh bumps both). [workspace.package]-version = "2.0.686"+version = "2.0.688" edition = "2021" license = "MIT"
rust/dashboard/src/lib.rs+4−3@@ -193,9 +193,10 @@ pub fn route(ctx: &std::sync::Arc<Ctx>, caller: &Caller, method: &str, path: &st ("GET", "/dashboard/icon.svg") => Some(Reply::Bytes(200, "image/svg+xml".into(), ICON_SVG.as_bytes().to_vec(), Some("public, max-age=3600".into()))), ("GET", "/favicon.ico") => Some(Reply::Bytes(200, "image/svg+xml".into(), ICON_SVG.as_bytes().to_vec(), Some("public, max-age=3600".into()))), ("GET", "/dashboard/newbie-panel.js") => Some(Reply::Bytes(200, "application/javascript".into(), assets_gen::get("newbie-panel.js").map(|b| b.to_vec()).unwrap_or_default(), Some("public, max-age=600".into()))),- ("GET", "/dashboard/example") => { let n = q(query, "name").unwrap_or_default(); assets_gen::get(&format!("icons/settings-ex/{n}.png")).map(|b| Reply::Bytes(200, "image/png".into(), b.to_vec(), Some("no-store".into()))) }- ("GET", "/dashboard/sample-favicon") => { let n = q(query, "name").unwrap_or_default(); let (file, ct) = match n.as_str() { "wiki" => ("wiki.svg", "image/svg+xml"), "ti" => ("ti.ico", "image/x-icon"), "mouser" => ("mouser.ico", "image/x-icon"), "digikey" => ("digikey.svg", "image/svg+xml"), _ => return None }; assets_gen::get(&format!("icons/settings-samples/{file}")).map(|b| Reply::Bytes(200, ct.into(), b.to_vec(), Some("public, max-age=3600".into()))) }- ("GET", "/dashboard/sample-cat") => { let n = q(query, "name").unwrap_or_default(); pup_identity::ICONS.iter().find(|(name, _)| *name == n || name.trim_end_matches(".ico") == n.trim_end_matches(".ico")).map(|(_, b)| Reply::Bytes(200, "image/x-icon".into(), b.to_vec(), Some("public, max-age=3600".into()))) }+ ("GET", "/dashboard/example") => { let n = q(query, "n").or_else(|| q(query, "name")).unwrap_or_default(); assets_gen::get(&format!("icons/settings-ex/{n}.png")).map(|b| Reply::Bytes(200, "image/png".into(), b.to_vec(), Some("no-store".into()))) }+ // 2.0.687: the page sends n=, the route read name=, so every Settings sample badge was a broken image+ ("GET", "/dashboard/sample-favicon") => { let n = q(query, "n").or_else(|| q(query, "name")).unwrap_or_default(); let (file, ct) = match n.as_str() { "wiki" => ("wiki.svg", "image/svg+xml"), "ti" => ("ti.ico", "image/x-icon"), "mouser" => ("mouser.ico", "image/x-icon"), "digikey" => ("digikey.svg", "image/svg+xml"), _ => return None }; assets_gen::get(&format!("icons/settings-samples/{file}")).map(|b| Reply::Bytes(200, ct.into(), b.to_vec(), Some("public, max-age=3600".into()))) }+ ("GET", "/dashboard/sample-cat") => { let n = q(query, "n").or_else(|| q(query, "name")).unwrap_or_default(); pup_identity::ICONS.iter().find(|(name, _)| *name == n || name.trim_end_matches(".ico") == n.trim_end_matches(".ico")).map(|(_, b)| Reply::Bytes(200, "image/x-icon".into(), b.to_vec(), Some("public, max-age=3600".into()))) } ("GET", "/dashboard/stampicon") => { let sid = q(query, "session").unwrap_or_default(); match tile_for(ctx, &sid) { Some(b) => Some(Reply::Bytes(200, "image/x-icon".into(), b, Some("public, max-age=60".into()))), None => Some(Reply::Bytes(200, "image/x-icon".into(), pup_identity::ICONS[1].1.to_vec(), Some("public, max-age=60".into()))) } } ("GET", "/dashboard/badge") | ("GET", "/dashboard/bestbadge") => { let sid = q(query, "session").unwrap_or_default(); let f = ctx.host.temp_dir().join("pup-badge-debug").join(format!("{}.png", pup_session::sanitize_sid(&sid))); match std::fs::read(f) { Ok(b) => png(b, "public, max-age=30"), Err(_) => png(png_1x1(), "no-store") } } ("GET", "/dashboard/baseicon") => Some(Reply::Bytes(200, "image/x-icon".into(), pup_identity::ICONS[0].1.to_vec(), Some("public, max-age=3600".into()))),
rust/identity/src/tile.rs+32−3@@ -116,6 +116,33 @@ pub fn draw(f: &Font, lines: &[String], bf: f32, s: u32) -> Vec<u8> { px } +/// 2.0.687 (John, 2026-10-03: "the rendered text in your taskbar icon still looks grainy; it used to look smoother").+/// The browser renderer it replaced (pre-2.0.559) placed glyphs at sub-pixel positions and blended in linear light.+/// draw() snaps every glyph to a whole pixel and blends in sRGB, which at 24-32 px makes uneven letter spacing and+/// blotchy stems. Draw the tile at SS x its size, then area-average it down (premultiplied alpha, gamma space, as+/// browsers blend text): glyph positions become exact to 1/SS px and the edges get real coverage. The plate gets the same benefit.+const SS: u32 = 4;+pub fn draw_smooth(f: &Font, lines: &[String], bf: f32, s: u32) -> Vec<u8> {+ let big = draw(f, lines, bf, s * SS);+ let bs = s * SS;+ let mut out = vec![0u8; (s * s * 4) as usize];+ let n = (SS * SS) as f32;+ for y in 0..s { for x in 0..s {+ let (mut r, mut g, mut b, mut a) = (0.0f32, 0.0f32, 0.0f32, 0.0f32);+ for sy in 0..SS { for sx in 0..SS {+ let i = (((y * SS + sy) * bs + (x * SS + sx)) * 4) as usize;+ let al = big[i + 3] as f32 / 255.0;+ // gamma-space average (what browsers do for text): linear-light averaging measurably THINNED the dark+ // strokes on the bright plate (tried first; the letters went faint)+ r += big[i] as f32 / 255.0 * al; g += big[i + 1] as f32 / 255.0 * al; b += big[i + 2] as f32 / 255.0 * al; a += al;+ } }+ let o = ((y * s + x) * 4) as usize;+ if a > 0.0 { out[o] = ((r / a).clamp(0.0, 1.0) * 255.0).round() as u8; out[o + 1] = ((g / a).clamp(0.0, 1.0) * 255.0).round() as u8; out[o + 2] = ((b / a).clamp(0.0, 1.0) * 255.0).round() as u8; }+ out[o + 3] = ((a / n) * 255.0).round() as u8;+ } }+ out+}+ /// Blit a line of text (left-aligned, alphabetic baseline) onto an RGBA buffer, glyphs rasterised by /// fontdue at exactly `fpx`, coverage-blended into RGB. Shared by the tile and the annotate caption bar. pub fn draw_text(f: &Font, px: &mut [u8], w: u32, h: u32, text: &str, fpx: f32, x0: f32, baseline: f32, ink: [u8; 3]) {@@ -142,7 +169,7 @@ pub fn render_ico(name: &str) -> Option<Vec<u8>> { let (lines, bf) = layout(&f, name); let mut raw: Vec<(u32, Vec<u8>)> = Vec::new(); let mut png256: Vec<u8> = Vec::new(); for &sz in crate::TILE_SIZES.iter() {- let px = draw(&f, &lines, bf, sz);+ let px = if sz >= 256 { draw(&f, &lines, bf, sz) } else { draw_smooth(&f, &lines, bf, sz) }; if sz >= 256 { png256 = crate::png_encode_rgba(&px, sz, sz); } else { raw.push((sz, px)); } } Some(crate::ico_from_tiles(&raw, &png256))@@ -158,9 +185,11 @@ mod tests { for name in ["pup-bridge", "pup-ab-smoke", "kicad-bridge-esc-routing", "bt556", "sxsw", "Logic gate molecule family"] { let (lines, bf) = layout(&f, name); assert!(!lines.is_empty() && lines.len() <= 2 && bf > 0.0, "{name}: {lines:?} {bf}");- for sz in [256u32, 48, 32, 16] {+ for sz in [256u32, 48, 32, 24, 16] { let px = draw(&f, &lines, bf, sz);- std::fs::write(dir.join(format!("{}-{sz}.png", crate::thread_slug(name))), crate::png_encode_rgba(&px, sz, sz)).unwrap();+ std::fs::write(dir.join(format!("{}-{sz}-old.png", crate::thread_slug(name))), crate::png_encode_rgba(&px, sz, sz)).unwrap();+ let px2 = draw_smooth(&f, &lines, bf, sz);+ std::fs::write(dir.join(format!("{}-{sz}.png", crate::thread_slug(name))), crate::png_encode_rgba(&px2, sz, sz)).unwrap(); } let ico = render_ico(name).expect("ico"); assert!(ico.len() > 2000, "{name}: ico {} bytes", ico.len());
rust/verbs/assets/verb-catalog.json+9−11@@ -1890,7 +1890,10 @@ }, "hint": "Delete LEAKED isolated profile directories \u2014 the iso-* throwaway jars minted by isolated:true opens (and by wikiView:public before 2.0.132, when the public view moved to the server-side ?view=public param) whose window is long gone (they used to pile up, 100s per machine). Only removes an iso-* dir that no live session or session file owns, that no Chrome is holding, and that is >15 min old; durable jars (adom-you, named profiles) are NEVER touched. Runs automatically every 15 min too. dryRun:true previews the list without deleting.", "input": {- "dryRun": "optional bool \u2014 preview which iso-* profiles would be removed, delete nothing"+ "dryRun": "optional bool: list, delete nothing",+ "staleDays": "optional int, default 14",+ "isoOnly": "optional bool: only throwaway iso-* profiles",+ "all": "optional bool: ignore the idle age (still never a protected, open or needed profile)" }, "longRunning": false, "name": "pup_reap_profiles",@@ -1900,7 +1903,7 @@ "pitfalls": [], "related": [], "statusVerb": null,- "summary": "Delete LEAKED isolated profile directories \u2014 the iso-* throwaway jars minted by isolated:true opens (and by wikiView:public before 2.0.132, when the public view moved to the server-side ?view=public param) whose window is long gone (they used to pile up, 100s per machine). Only removes an iso-* dir that no live session or session file owns, that no Chrome is holding, and that is >15 min old; durable jars (adom-you, named profiles) are NEVER touched. Runs automatically every 15 min too. dryRun:true previews the list without deleting.",+ "summary": "Delete pup profiles nobody uses: by default every profile idle for staleDays (14) that is not protected (adom-you, adom-wiki-authed, pup-dashboard, _recorder), not open in a browser, and not needed to restore a saved window; with isoOnly:true only leaked throwaway iso-* profiles. dryRun:true lists without deleting. pup's own boot pass runs the isoOnly form.", "timeoutSeconds": 20 }, {@@ -1940,16 +1943,11 @@ }, { "example": {- "dryRun": true+ "confirm": true },- "hint": "DESTRUCTIVE \u2014 cleanly remove pup's WHOLE footprint from this machine: the HKCU AppUserModelId Adom.Pup registry keys, all staged extensions, ALL pup profiles + sessions, the OS-keychain credential vault, pup-settings.json, the badge-debug temp dir, and the jump-list launcher. ALWAYS preview first with dryRun:true (removes nothing, returns the plan). To actually remove, pass confirm:\"uninstall\". keepCredentials:true preserves the saved-login vault; keepProfiles:true preserves profiles/sessions. Chrome-for-Testing + firewall rules are SHARED and left alone unless removeChrome:true / removeFirewallRules:true. Closes every window + quits every browser first to release profile locks. (The bridge itself is managed by Adom Bridge \u2014 remove pup from Bridge to stop it re-seeding.)",+ "hint": "There is no dry run: list what you would lose first with pup_status (profiles, windows) and pup_credentials.", "input": {- "confirm": "required str \"uninstall\" to actually remove",- "dryRun": "optional bool \u2014 preview the removal plan without removing anything (do this first)",- "keepCredentials": "optional bool \u2014 keep the saved-login vault",- "keepProfiles": "optional bool \u2014 keep pup profiles + sessions",- "removeChrome": "optional bool \u2014 also delete the Chrome-for-Testing install (shared; default keep)",- "removeFirewallRules": "optional bool \u2014 also remove the CfT firewall rules (needs admin)"+ "confirm": "required bool true: actually remove" }, "longRunning": false, "name": "pup_uninstall",@@ -1959,7 +1957,7 @@ "pitfalls": [], "related": [], "statusVerb": null,- "summary": "DESTRUCTIVE \u2014 cleanly remove pup's WHOLE footprint from this machine: the HKCU AppUserModelId Adom.Pup registry keys, all staged extensions, ALL pup profiles + sessions, the OS-keychain credential vault, pup-settings.json, the badge-debug temp dir, and the jump-list launcher. ALWAYS preview first with dryRun:true (removes nothing, returns the plan). To actually remove, pass confirm:\"uninstall\". keepCredentials:true preserves the saved-login vault; keepProfiles:true preserves profiles/sessions. Chrome-for-Testing + firewall rules are SHARED and left alone unless removeChrome:true / removeFirewallRules:true. Closes every window + quits every browser first to release profile locks. (The bridge itself is managed by Adom Bridge \u2014 remove pup from Bridge to stop it re-seeding.)",+ "summary": "DESTRUCTIVE: remove pup's on-disk footprint from this machine: every pup profile and saved window, the staged extensions, the taskbar icons and tiles, and every taskbar identity pup registered. Pass confirm:true. The saved-login vault is kept (credential_delete removes logins one by one), and Adom Bridge's bridge_uninstall removes the pup program itself.", "timeoutSeconds": 60 }, {
tools/gen-assets.py+18@@ -150,6 +150,24 @@ _STUBS = { 'credential_delete': lambda: _entry('credential_delete', "Remove a saved login from pup's vault.", {'host': 'str', 'username': 'optional str: only this account'}, {'host': 'example.com'}, 'Removes the saved password from the vault only; a session the profile is already signed into stays signed in (pup_clear_cookies ends that).', ['pup_credentials', 'credential_set']), 'pup_launch': lambda: dict(_entry('pup_launch', '', {}, {}, '', ['pup_open_window'], like='pup_open_window'), summary='Alias of pup_open_window (url defaults to about:blank).'), }+# 2.0.687: entries whose text came from the Node build but whose Rust behavior differs. An AI reads these as the+# contract, so they must describe what the exe does (pup_uninstall took confirm:"uninstall" and offered a dry run in+# Node; the Rust verb takes confirm:true and has no dry run).+_OVERRIDES = {+ 'pup_uninstall': {+ 'summary': "DESTRUCTIVE: remove pup's on-disk footprint from this machine: every pup profile and saved window, the staged extensions, the taskbar icons and tiles, and every taskbar identity pup registered. Pass confirm:true. The saved-login vault is kept (credential_delete removes logins one by one), and Adom Bridge's bridge_uninstall removes the pup program itself.",+ 'input': {'confirm': 'required bool true: actually remove'},+ 'example': {'confirm': True},+ 'hint': 'There is no dry run: list what you would lose first with pup_status (profiles, windows) and pup_credentials.',+ },+ 'pup_reap_profiles': {+ 'summary': "Delete pup profiles nobody uses: by default every profile idle for staleDays (14) that is not protected (adom-you, adom-wiki-authed, pup-dashboard, _recorder), not open in a browser, and not needed to restore a saved window; with isoOnly:true only leaked throwaway iso-* profiles. dryRun:true lists without deleting. pup's own boot pass runs the isoOnly form.",+ 'input': {'dryRun': 'optional bool: list, delete nothing', 'staleDays': 'optional int, default 14', 'isoOnly': 'optional bool: only throwaway iso-* profiles', 'all': 'optional bool: ignore the idle age (still never a protected, open or needed profile)'},+ 'example': {'dryRun': True},+ },+}+for _v in _verbs:+ if _v['name'] in _OVERRIDES: _v.update(_OVERRIDES[_v['name']]) _missing = [] for _n in _routed: if _n in _have: continue