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.
master
John Lauer
v2.0.734 (insiders): pup keeps its own browser on the current Chrome Stable, silently (John, 2026-10-25: 'New Chrome av…
f278a89
16d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
//! Preference verbs: pup_configure (get/set the ~/.adom/pup-settings.json keys, same names and
//! defaults as the Node build), pup_use (pin the browser), pup_quit_idle_browsers.
use crate::Ctx;
use pup_core::{err, ok};
use serde_json::{json, Value};
use std::time::Duration;
/// key -> (default, allowed values or "int" or "str")
pub const KEYS: &[(&str, &str, &str)] = &[
("taskbarMode", "tiles", "tiles|aumid|plain"), ("taskbarGrouping", "split", "split|grouped"), ("aumidIconStyle", "category", "standard|category"),
("aumidIcons", "on", "on|off"), ("threadTiles", "on", "on|off"), ("overlayBadges", "on", "on|off"), ("overlaySize", "75", "50|66|75|100"),
("jumpLists", "on", "on|off"), ("jlViewTasks", "on", "on|off"), ("jlCloseAll", "on", "on|off"), ("jlDashboard", "on", "on|off"), ("jlAnnotate", "on", "on|off"),
("toolbarExtension", "on", "on|off"), ("tbSnipHotkey", "on", "on|off"), ("tbFloatyPencil", "off", "on|off"), ("floatyHitPx", "24", "int"), ("tbOwnerBadge", "on", "on|off"),
("annCaptionBar", "on", "on|off"),
("taskbarActivity", "progress", "progress|flash|off"),
("defaultLane", "ext", "ext|cdp"),
("consumerBrands", "on", "on|off"),
("laneStockFlags", "on", "on|off"),
("cdpLane", "off", "on|off"),
("importTipBelow", "5", "int"),
("browserAutoUpdate", "on", "on|off"), // 2.0.734: keep pup's browser on the current Chrome Stable // 2.0.733: the first-window import tip shows while the vault has fewer saved logins than this // 2.0.652 (John): the CDP lane is opt-in per box; the extension lane must solve every use case
("laneFieldTrials", "on", "on|off"),
("consumerBrandsHeader", "", "str"),
("fgWatch", "on", "on|off"), ("birthSettleMs", "400", "int"), ("aumidShortcut", "off", "on|off"), ("idleBrowserQuitMin", "10", "int"), ("winAttentionFlash", "off", "on|off"), ("prePermissions", "off", "on|off"), ("cdpQuiet", "off", "on|off"), ("stockFlags", "off", "on|off"), ("fgCaption", "on", "on|off"), ("fgCaptionMs", "3500", "int"), ("notMineCaption", "off", "on|off"),
("captureLogins", "on", "on|off"), ("autoLogin", "on", "on|off"),
("ownerChip", "off", "on|off"), ("titleTag", "off", "on|off"), ("titlePrefix", "off", "on|off"),
("sleepRecovery", "on", "on|off"), ("reportIssues", "on", "on|off"), ("minimalTouch", "on", "on|off"),
];
pub fn snapshot(ctx: &Ctx) -> Value {
let mut m = serde_json::Map::new();
for (k, d, _) in KEYS { m.insert(k.to_string(), json!(ctx.settings.get(k, d))); }
// taskbarMode is derived from the two underlying keys when not set explicitly (Node ladder)
let mode = ctx.settings.all().get("taskbarMode").and_then(|v| v.as_str()).map(|s| s.to_string()).unwrap_or_else(|| if ctx.settings.get("aumidIcons", "on") != "on" { "plain".into() } else if ctx.settings.get("threadTiles", "on") == "on" { "tiles".into() } else { "aumid".into() });
m.insert("taskbarMode".into(), json!(mode));
Value::Object(m)
}
pub fn configure(ctx: &Ctx, args: &Value) -> Value {
let mut changed = Vec::new(); let mut rejected = Vec::new();
if let Some(o) = args.as_object() {
for (k, v) in o {
if k == "caller" || k.starts_with('_') { continue; } // CLI/relay bookkeeping, not settings
if ["openFlash", "autoFlash", "agentActivity", "fgWatch"].contains(&k.as_str()) { rejected.push(json!({ "key": k, "why": format!("{k} was removed in 2.0.292; use taskbarActivity: \"progress\"|\"flash\"|\"off\" (and notMineCaption for the foreground caption)") })); continue }
let Some((_, _, allowed)) = KEYS.iter().find(|(kk, _, _)| kk == k) else { rejected.push(json!({ "key": k, "why": "unknown setting" })); continue };
let sv = match v { Value::String(s) => s.clone(), Value::Bool(b) => if *b { "on".into() } else { "off".into() }, Value::Number(n) => n.to_string(), _ => { rejected.push(json!({ "key": k, "why": "value must be a string, bool or number" })); continue } };
let okv = match *allowed { "int" => sv.parse::<i64>().is_ok(), "str" => true, list => list.split('|').any(|a| a == sv) };
if !okv { rejected.push(json!({ "key": k, "why": format!("value must be one of {allowed}"), "got": sv })); continue }
let _ = ctx.settings.set(k, json!(sv));
changed.push(k.clone());
// the taskbarMode ladder writes its two underlying keys (Node: taskbarMode maps onto them)
if k == "taskbarMode" { let (a, t) = match sv.as_str() { "plain" => ("off", "off"), "aumid" => ("on", "off"), _ => ("on", "on") }; let _ = ctx.settings.set("aumidIcons", json!(a)); let _ = ctx.settings.set("threadTiles", json!(t)); }
}
}
// live apply (Node: a mode switch re-identifies every window; a badge setting repaints every badge)
const RESTAMP: [&str; 11] = ["taskbarMode", "aumidIconStyle", "taskbarGrouping", "threadTiles", "aumidIcons", "jumpLists", "jlViewTasks", "jlCloseAll", "jlDashboard", "jlAnnotate", "aumidShortcut"];
if let Some(a) = ctx.arc() {
if changed.iter().any(|k| RESTAMP.contains(&k.as_str())) { crate::identity::reapply_all(a, true); }
else if changed.iter().any(|k| k == "overlayBadges" || k == "overlaySize") { crate::identity::reapply_all(a, false); }
}
let out = json!({ "ok": true, "settings": snapshot(ctx), "changed": changed, "rejected": rejected, "platform": if cfg!(windows) { "win32" } else if cfg!(target_os = "macos") { "darwin" } else { "linux" }, "taskbarSupported": cfg!(windows), "_hint": if changed.is_empty() { "Current pup preferences (persisted in ~/.adom/pup-settings.json). Pass {key:value} to change one; every key applies live, no restart." } else { "Saved. Live windows pick the change up on their next identity pass; toolbarExtension changes apply to the NEXT browser launch." } });
ok(json!({ "success": true, "output": out.to_string(), "settings": out["settings"], "changed": out["changed"], "rejected": out["rejected"] }))
}
pub fn use_browser(ctx: &Ctx, args: &Value) -> Value {
let cands = pup_browser::launch_candidates(&*ctx.host, ctx.prefer_cft());
let want = args.get("browser").and_then(|b| b.as_str()).unwrap_or("").to_ascii_lowercase();
let temporary = args.get("temporary").and_then(|b| b.as_bool()).unwrap_or(false);
let picked = match want.as_str() {
"" | "auto" => {
let p = ctx.host.cft_cache_dir().join("pup-browser-default.json"); let _ = std::fs::remove_file(p);
return ok(json!({ "success": true, "cleared": true, "picked": cands.first().map(|c| c.to_json()), "candidates": cands.iter().map(|c| c.to_json()).collect::<Vec<_>>(), "defaultBrowser": Value::Null, "output": json!({ "ok": true, "cleared": true, "picked": cands.first().map(|c| c.to_json()), "candidates": cands.iter().map(|c| c.to_json()).collect::<Vec<_>>(), "defaultBrowser": Value::Null, "readiness": ctx.installer.readiness(&*ctx.host, ctx.prefer_cft()), "_hint": "Default pin cleared: pup picks Chrome for Testing when cached (the toolbar build), else the installed browser. Relaunch windows to converge." }).to_string(), "_hint": "Default pin cleared: pup picks Chrome for Testing when cached (the toolbar build), else the installed browser. Relaunch windows to converge." }));
}
"cft" | "chrome-for-testing" => {
match pup_browser::detect_cft(&*ctx.host) { Some(c) => c, None => { ctx.installer.ensure_background(ctx.host.clone()); return pup_core::merge(err("chrome_for_testing_installing", "Chrome for Testing is not cached yet; fetching it now", Some("Poll pup_readiness until chromeForTestingInstalled:true, then pup_use {browser:\"cft\"} again (or just open a window: it converges on its own).")), json!({ "installing": true, "stillRunning": true, "statusVerb": "pup_readiness" })); } }
}
"chrome" if args.get("install").and_then(|b| b.as_bool()) == Some(true) && cands.iter().all(|c| c.kind != "chrome") => {
let elevate = match args.get("elevate") { Some(Value::Bool(b)) => Some(*b), Some(Value::String(s)) if s == "false" => Some(false), Some(Value::String(s)) if s == "true" => Some(true), _ => None };
if let Some(a) = ctx.arc() { pup_browser::chrome_stable::install_background(a.host.clone(), elevate); }
return pup_core::merge(err("chrome_stable_installing", "Installing real Google Chrome in the background (~90 MB). On a locked-down box it needs a Windows approval (UAC).", Some("Poll pup_readiness: chromeStablePhase goes installing -> awaiting_uac -> ready. pup drives the existing browser until then.")), json!({ "installingChrome": true, "readiness": ctx.installer.readiness(&*ctx.host, ctx.prefer_cft()) }));
}
"chrome" | "edge" => match cands.iter().find(|c| c.kind == want) { Some(c) => c.clone(), None => return err("browser_not_found", format!("no installed {want} found on this box"), Some("pup_readiness lists the candidates pup can see.")) },
other => { let p = std::path::PathBuf::from(other); if p.is_file() { pup_browser::Candidate { kind: "custom".into(), source: "explicit".into(), executable_path: p, build_id: None } } else { return err("bad_browser", "browser must be chrome | edge | cft | auto, or an executable path", None) } }
};
pup_browser::write_default(&*ctx.host, &picked, true);
if temporary { let p = ctx.host.cft_cache_dir().join("pup-browser-default.json"); if let Ok(s) = std::fs::read_to_string(&p) { if let Ok(mut v) = serde_json::from_str::<Value>(&s) { v["temporary"] = json!(true); let _ = std::fs::write(&p, v.to_string()); } } }
let use_hint = if picked.kind != "chrome-for-testing" { "Pinned. NOTE: the Adom toolbar rides ONLY Chrome for Testing; windows on this browser have no toolbar. A window already open keeps its browser; new launches use the pin." } else { "Pinned Chrome for Testing as the default. The next pup_open_window drives it." };
let out = json!({ "ok": true, "picked": picked.to_json(), "cleared": false, "temporary": temporary, "candidates": cands.iter().map(|c| c.to_json()).collect::<Vec<_>>(), "defaultBrowser": pup_browser::read_default(&*ctx.host), "readiness": ctx.installer.readiness(&*ctx.host, ctx.prefer_cft()), "_hint": use_hint });
ok(json!({ "success": true, "picked": picked.to_json(), "cleared": false, "candidates": cands.iter().map(|c| c.to_json()).collect::<Vec<_>>(), "defaultBrowser": pup_browser::read_default(&*ctx.host), "output": out.to_string(),
"_hint": if picked.kind != "chrome-for-testing" { "Pinned. NOTE: the Adom toolbar rides ONLY Chrome for Testing; windows on this browser have no toolbar. A window already open keeps its browser; new launches use the pin." } else { "Pinned to Chrome for Testing (the toolbar build). New launches use it." } }))
}
pub fn quit_idle_browsers(ctx: &Ctx) -> Value {
let idle: Vec<String> = ctx.browsers.lock().unwrap().keys().filter(|p| ctx.sessions.for_profile(p).is_empty()).cloned().collect();
let mut quit = Vec::new();
for p in idle {
if let Some(b) = ctx.browsers.lock().unwrap().remove(&p) {
let _ = b.cdp.send(None, "Browser.close", json!({}), Duration::from_secs(5)); b.cdp.close();
if b.launched.pid > 0 { std::thread::sleep(Duration::from_millis(800)); if ctx.host.alive(b.launched.pid) { ctx.host.kill(b.launched.pid); } }
quit.push(p);
}
}
ok(json!({ "quit": quit, "count": quit.len(), "remaining": ctx.browsers.lock().unwrap().len(), "_hint": "Browsers with no pup windows were closed; the next open launches a fresh one (with any new extension code)." }))
}