← Commit history

v2.0.643 (insiders): landed with a DOM probe (Log in link = not signed in; login form = sign-in page), on open/navigate/open_tab

John Lauer ·5598ee365f ·1mo ago ·parent b530c67
4 files changed +43−23
rust/Cargo.lock+12−12
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-pup-bridge"-version = "2.0.642"+version = "2.0.643" dependencies = [  "pup-browser",  "pup-cdp",@@ -655,7 +655,7 @@ dependencies = [  [[package]] name = "pup-browser"-version = "2.0.642"+version = "2.0.643" dependencies = [  "pup-core",  "pup-host",@@ -667,7 +667,7 @@ dependencies = [  [[package]] name = "pup-cdp"-version = "2.0.642"+version = "2.0.643" dependencies = [  "serde_json",  "thiserror",@@ -677,7 +677,7 @@ dependencies = [  [[package]] name = "pup-core"-version = "2.0.642"+version = "2.0.643" dependencies = [  "serde",  "serde_json",@@ -687,7 +687,7 @@ dependencies = [  [[package]] name = "pup-dashboard"-version = "2.0.642"+version = "2.0.643" dependencies = [  "pup-cdp",  "pup-core",@@ -700,7 +700,7 @@ dependencies = [  [[package]] name = "pup-host"-version = "2.0.642"+version = "2.0.643" dependencies = [  "serde",  "serde_json",@@ -709,14 +709,14 @@ dependencies = [  [[package]] name = "pup-host-mac"-version = "2.0.642"+version = "2.0.643" dependencies = [  "pup-host", ]  [[package]] name = "pup-host-win"-version = "2.0.642"+version = "2.0.643" dependencies = [  "pup-core",  "pup-host",@@ -725,7 +725,7 @@ dependencies = [  [[package]] name = "pup-identity"-version = "2.0.642"+version = "2.0.643" dependencies = [  "fontdue",  "image",@@ -735,7 +735,7 @@ dependencies = [  [[package]] name = "pup-session"-version = "2.0.642"+version = "2.0.643" dependencies = [  "pup-core",  "serde",@@ -745,7 +745,7 @@ dependencies = [  [[package]] name = "pup-vault"-version = "2.0.642"+version = "2.0.643" dependencies = [  "pup-core",  "pup-host",@@ -756,7 +756,7 @@ dependencies = [  [[package]] name = "pup-verbs"-version = "2.0.642"+version = "2.0.643" 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.642"+version = "2.0.643" edition = "2021" license = "MIT" 
rust/verbs/src/lib.rs+28−8
@@ -1351,7 +1351,23 @@ fn navigate(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { /// 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 {+pub fn landed(url: &str, title: &str) -> Value { landed_with(url, title, None) }++/// 2.0.643: the DOM probe behind `landed`. (login_offered, login_form): a visible "Log in"/"Sign in" link or+/// button means the profile is NOT signed in here even when the URL looks like an app page (chatgpt.com/codex+/// is a marketing page with a Log in link when signed out, and the URL rule alone called it signed in); a+/// password field, or an email field next to a Continue/Next button, means the page IS a sign-in form.+pub fn landed_ext(ctx: &Ctx, s: &Session, tab: &pup_session::Tab, url: &str, title: &str) -> Value {+    const PROBE: &str = "(()=>{try{const vis=e=>{const r=e.getBoundingClientRect();return r.width>0&&r.height>0};const txt=e=>((e.innerText||e.value||e.getAttribute('aria-label')||'')+'').trim().toLowerCase().replace(/\\s+/g,' ');const els=[...document.querySelectorAll('a,button,input[type=submit],[role=button]')].filter(vis);const offered=els.some(e=>/^(log ?in|sign ?in|log ?in with .*|sign ?in with .*)$/.test(txt(e)));const pw=!!document.querySelector('input[type=password]');const em=!!document.querySelector('input[type=email],input[name*=email i],input[autocomplete=username],input[autocomplete=email]');const cont=els.some(e=>/^(continue|next|log ?in|sign ?in|continue with .*)$/.test(txt(e)));return JSON.stringify({offered,form:pw||(em&&cont)})}catch(e){return JSON.stringify({err:String(e)})}})()";+    let probe = transport::lane_for(ctx, s).ok().and_then(|lane| transport::evaluate(&lane, tab, PROBE).ok()).and_then(|v| {+        let txt = v.as_str().map(|x| x.to_string()).or_else(|| v.get("result").and_then(|r| r.as_str()).map(|x| x.to_string()))?;+        let j: Value = serde_json::from_str(&txt).ok()?;+        Some((j["offered"].as_bool().unwrap_or(false), j["form"].as_bool().unwrap_or(false)))+    });+    landed_with(url, title, probe)+}++pub fn landed_with(url: &str, title: &str, probe: Option<(bool, bool)>) -> 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());@@ -1360,17 +1376,21 @@ pub fn landed(url: &str, title: &str) -> Value {         || 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 {+    let (offered, form) = probe.unwrap_or((false, false));+    let login_page = known_signin || generic || form;+    let signed_in_known = !login_page && !offered && (+        (host.ends_with("claude.ai") && (path.starts_with("/new") || path.starts_with("/chat") || path.starts_with("/project") || path.starts_with("/settings") || path == "/"))+        || (host.ends_with("chatgpt.com") && (path.starts_with("/c/") || path.starts_with("/g/") || (path.starts_with("/codex") && probe.is_some()))));+    let hint = if !login_page && offered {+        format!("The page landed on {url} and shows a Log in / Sign in link: the shared adom-you profile is NOT signed in here. Follow login-recipes/<domain>.md on the pup-bridge wiki page (Google sign-in via pup_google_signin, or the vault via pup_login) before involving the user.")+    } else 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 })+    json!({ "url": url, "title": title, "loginPage": login_page, "loginOffered": offered, "signedIn": if signed_in_known { json!(true) } else if login_page || offered { json!(false) } else { Value::Null }, "probed": probe.is_some(), "_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 };@@ -1391,7 +1411,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", "landed": landed(&u, &title) }))+    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_ext(ctx, s, &tab, &u, &title) })) }  fn eval(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {@@ -1799,7 +1819,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", "landed": landed(&t.url, &t.title),+    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_ext(ctx, &s, &t, &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." })) } 
rust/verbs/src/tabs.rs+2−2
@@ -27,7 +27,7 @@ pub fn open_tab(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {             if let Some(t) = s.tabs.iter().find(|t| t.url.trim_end_matches('/') == url.trim_end_matches('/')).cloned() {                 let _ = crate::transport::activate(&lane, &t);                 ctx.sessions.with_mut(&s.id, |x| { if let Some(i) = x.tabs.iter().position(|tt| tt.tab_id == t.tab_id) { x.active_tab = i; } });-                return ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tab": tab_row(ctx, &s, &t, true), "tabId": t.tab_id, "count": s.tabs.len(), "transport": "ext", "reused": true, "_hint": "This window already had a tab at that URL; it was activated instead of opening a duplicate." }));+                return ok(json!({ "sessionId": s.id, "window": s.id, "aiThread": s.owner, "tab": tab_row(ctx, &s, &t, true), "tabId": t.tab_id, "count": s.tabs.len(), "transport": "ext", "reused": true, "landed": crate::landed_ext(ctx, &s, &t, &t.url, &t.title), "_hint": "This window already had a tab at that URL; it was activated instead of opening a duplicate." }));             }         }         let mut sess = s.clone();@@ -37,7 +37,7 @@ pub fn open_tab(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {         ctx.sessions.insert(sess.clone()); ctx.sessions.save_file(&sess);         crate::identity::overlay(ctx, &sess.id, caller);         touch(ctx, &sess.id, args.get("updateNote").and_then(|n| n.as_str()));-        return ok(json!({ "sessionId": sess.id, "window": sess.id, "aiThread": sess.owner, "tab": tab_row(ctx, &sess, &t, true), "tabId": t.tab_id, "count": sess.tabs.len(), "transport": "ext" }));+        return ok(json!({ "sessionId": sess.id, "window": sess.id, "aiThread": sess.owner, "tab": tab_row(ctx, &sess, &t, true), "tabId": t.tab_id, "count": sess.tabs.len(), "transport": "ext", "landed": crate::landed_ext(ctx, &sess, &t, &t.url, &t.title) }));     }     let b = match browser_for(ctx, &s.profile) { Some(b) => b, None => return err("browser_gone", "the browser behind this window is not connected", None) };     let url = args.get("url").and_then(|u| u.as_str()).map(|u| u.trim()).filter(|u| !u.is_empty()).unwrap_or("about:blank").to_string();