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.642 (insiders): landed hint on open/navigate (never ask the user to sign in where the profile already is); claude.ai + chatgpt.com login recipes; carries 2.0.641's one-frame birth
3 files changed
+41−15
rust/Cargo.lock+12−12@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-pup-bridge"-version = "2.0.641"+version = "2.0.642" dependencies = [ "pup-browser", "pup-cdp",@@ -655,7 +655,7 @@ dependencies = [ [[package]] name = "pup-browser"-version = "2.0.641"+version = "2.0.642" dependencies = [ "pup-core", "pup-host",@@ -667,7 +667,7 @@ dependencies = [ [[package]] name = "pup-cdp"-version = "2.0.641"+version = "2.0.642" dependencies = [ "serde_json", "thiserror",@@ -677,7 +677,7 @@ dependencies = [ [[package]] name = "pup-core"-version = "2.0.641"+version = "2.0.642" dependencies = [ "serde", "serde_json",@@ -687,7 +687,7 @@ dependencies = [ [[package]] name = "pup-dashboard"-version = "2.0.641"+version = "2.0.642" dependencies = [ "pup-cdp", "pup-core",@@ -700,7 +700,7 @@ dependencies = [ [[package]] name = "pup-host"-version = "2.0.641"+version = "2.0.642" dependencies = [ "serde", "serde_json",@@ -709,14 +709,14 @@ dependencies = [ [[package]] name = "pup-host-mac"-version = "2.0.641"+version = "2.0.642" dependencies = [ "pup-host", ] [[package]] name = "pup-host-win"-version = "2.0.641"+version = "2.0.642" dependencies = [ "pup-core", "pup-host",@@ -725,7 +725,7 @@ dependencies = [ [[package]] name = "pup-identity"-version = "2.0.641"+version = "2.0.642" dependencies = [ "fontdue", "image",@@ -735,7 +735,7 @@ dependencies = [ [[package]] name = "pup-session"-version = "2.0.641"+version = "2.0.642" dependencies = [ "pup-core", "serde",@@ -745,7 +745,7 @@ dependencies = [ [[package]] name = "pup-vault"-version = "2.0.641"+version = "2.0.642" dependencies = [ "pup-core", "pup-host",@@ -756,7 +756,7 @@ dependencies = [ [[package]] name = "pup-verbs"-version = "2.0.641"+version = "2.0.642" 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.641"+version = "2.0.642" edition = "2021" license = "MIT"
rust/verbs/src/lib.rs+28−2@@ -1346,6 +1346,32 @@ fn navigate(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { } /// The extension-lane arm of pup_navigate: same session bookkeeping, driven through the transport.++/// 2.0.642: where the page actually LANDED, classified, so an AI never asks the user to sign in to a site the+/// shared profile is already signed into. John, 2026-09-19: another thread opened claude.ai, which went straight+/// to his chats (the adom-you profile had the session), and it toasted "Sign into claude.ai in the pup window+/// once" anyway. The open/navigate response now says whether the final page is a sign-in page.+pub fn landed(url: &str, title: &str) -> Value {+ let u = url.to_ascii_lowercase(); let t = title.to_ascii_lowercase();+ let host = u.split("//").nth(1).unwrap_or("").split('/').next().unwrap_or("").to_string();+ let path = u.split("//").nth(1).unwrap_or("").splitn(2, '/').nth(1).map(|p| format!("/{p}")).unwrap_or_else(|| "/".into());+ let known_signin = (host.ends_with("claude.ai") && (path.starts_with("/login") || path.starts_with("/magic-link") || path.starts_with("/oauth")))+ || ((host.ends_with("chatgpt.com") || host.ends_with("openai.com")) && (path.contains("/auth/") || path.starts_with("/log-in") || path.starts_with("/login") || host.starts_with("auth.")))+ || host == "accounts.google.com" || host.ends_with("login.microsoftonline.com") || host.ends_with("appleid.apple.com");+ let generic = ["/login", "/signin", "/sign-in", "/sign_in", "/auth", "/sso", "/oauth", "/authorize", "/session/new", "/account/login", "/users/sign_in"].iter().any(|p| path.starts_with(p) || path.contains(&format!("{p}?")) || path.contains(&format!("{p}/")))+ || ["sign in", "log in", "login", "signin"].iter().any(|k| t.starts_with(k) || t.contains(&format!(" {k}")) && t.len() < 40);+ let login_page = known_signin || generic;+ let signed_in_known = (host.ends_with("claude.ai") && !login_page && (path.starts_with("/new") || path.starts_with("/chat") || path.starts_with("/project") || path.starts_with("/settings") || path == "/"))+ || (host.ends_with("chatgpt.com") && !login_page && (path.starts_with("/c/") || path.starts_with("/g/") || path.starts_with("/codex/") && !t.contains("log in")));+ let hint = if login_page {+ format!("The page landed on a SIGN-IN page ({url}). Before asking the user for anything: pup_credentials for a saved login (pup_login fills it), pup_google_accounts + pup_google_signin if the site offers Google sign-in, and login-recipes/<domain>.md on the pup-bridge wiki page. Toast the user ONLY if pup cannot finish the sign-in itself.")+ } else if signed_in_known {+ format!("The page landed on {url}: the shared adom-you profile is ALREADY signed in here. Do not ask the user to sign in and do not toast about signing in.")+ } else {+ format!("The page landed on {url}. It is not a sign-in page. Do not ask the user to sign in unless the page itself asks.")+ };+ json!({ "url": url, "title": title, "loginPage": login_page, "signedIn": if signed_in_known { json!(true) } else if login_page { json!(false) } else { Value::Null }, "_hint": hint })+} fn navigate_ext(ctx: &Ctx, caller: &Caller, s: &Session, url: &str) -> Value { let lane = match transport::lane_for(ctx, s) { Ok(l) => l, Err(e) => return e }; let tab = match s.active() { Some(t) => t.clone(), None => return err("no_tab", "the window has no tabs", None) };@@ -1365,7 +1391,7 @@ fn navigate_ext(ctx: &Ctx, caller: &Caller, s: &Session, url: &str) -> Value { // 2.0.629: force, so a navigation inside the birth grace that Chrome ACTIVATED is still bottomed under the // idle-hands rule; outside the grace, or with any input since birth, the primitive stands down as before. let _ = park::bottom_unless_foreground(ctx, &s.id, true);- ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": tab.tab_id, "url": u, "title": title, "loaded": loaded, "transport": "ext" }))+ ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": tab.tab_id, "url": u, "title": title, "loaded": loaded, "transport": "ext", "landed": landed(&u, &title) })) } fn eval(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {@@ -1773,7 +1799,7 @@ pub fn open_window_ext(ctx: &Ctx, caller: &Caller, args: &Value, sid: &str, owne } } let t = s.active().cloned().unwrap_or(t);- ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": t.tab_id, "url": t.url, "title": t.title, "hwnd": s.hwnd, "windowId": s.window_id, "transport": "ext",+ ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tabId": t.tab_id, "url": t.url, "title": t.title, "hwnd": s.hwnd, "windowId": s.window_id, "transport": "ext", "landed": landed(&t.url, &t.title), "_hint": "Extension-driven window (no CDP debug port): a first-class pup window. Drive it with pup_navigate / pup_eval / pup_screenshot / pup_open_tab like any other." })) }