diff --git a/src/botbrowser.rs b/src/botbrowser.rs index d74b9e7..656c5f4 100644 --- a/src/botbrowser.rs +++ b/src/botbrowser.rs @@ -542,15 +542,16 @@ pub fn release(job: &str) -> Vec { /// The tab of a page list (latest-used first) this step works on: its job's latest own tab, else /// the latest one no other job owns (a job's first step). +/// +/// A page that actually went somewhere (http/https) beats a blank one at every step: a browser +/// that boots on about:blank (headless Chrome does) must not strand a job on the empty tab. pub fn pick_tab(list: &[Value]) -> Option<&Value> { let me = job(); let id = |t: &Value| t["id"].as_str().unwrap_or("").to_string(); - if !me.is_empty() { - if let Some(t) = list.iter().find(|t| owner(&id(t)).as_deref() == Some(me.as_str())) { - return Some(t); - } - } - list.iter().find(|t| !others_tab(&id(t))) + let mine = |t: &Value| !me.is_empty() && owner(&id(t)).as_deref() == Some(me.as_str()); + let live = |t: &Value| t["url"].as_str().unwrap_or("").starts_with("http"); + let free = |t: &Value| !others_tab(&id(t)); + list.iter().find(|t| mine(t) && live(t)).or_else(|| list.iter().find(|t| free(t) && live(t))).or_else(|| list.iter().find(|t| mine(t))).or_else(|| list.iter().find(|t| free(t))) } /// The open page tabs' ids (none when the browser isn't open; it isn't started for this). @@ -889,6 +890,16 @@ mod tests { assert_eq!(as_job("job-b", || pick_tab(&list).map(|t| t["id"].clone())), Some("tab-b".into())); // A new job starts on the latest tab nobody has; another job's tabs are off limits. assert_eq!(as_job("job-c", || pick_tab(&list).map(|t| t["id"].clone())), Some("tab-free".into())); + // A real page beats a blank tab, whoever's it is: the blank-tab trap (headless Chrome + // boots on about:blank; a job that claims it must not be stranded there). + let pages = vec![serde_json::json!({"type": "page", "id": "blank", "url": "about:blank"}), serde_json::json!({"type": "page", "id": "real", "url": "https://example.com"})]; + assert_eq!(as_job("job-x", || { claim("blank"); pick_tab(&pages).map(|t| t["id"].clone()) }), Some("real".into())); + // A job's own real page still wins over a stranger's. + let mine = vec![serde_json::json!({"type": "page", "id": "theirs", "url": "https://other.example"}), serde_json::json!({"type": "page", "id": "mine", "url": "https://mine.example"})]; + assert_eq!(as_job("job-y", || { claim("mine"); pick_tab(&mine).map(|t| t["id"].clone()) }), Some("mine".into())); + // All blank: keep the old behaviour (own tab, then any free one). + let blanks = vec![serde_json::json!({"type": "page", "id": "b1", "url": "about:blank"}), serde_json::json!({"type": "page", "id": "b2", "url": "about:blank"})]; + assert_eq!(as_job("job-z", || { claim("b2"); pick_tab(&blanks).map(|t| t["id"].clone()) }), Some("b2".into())); assert!(as_job("job-c", || others_tab("tab-a") && !others_tab("tab-free"))); assert!(as_job("job-a", || !others_tab("tab-a"))); // A claimed tab stays its first owner's. diff --git a/src/browser.rs b/src/browser.rs index 37ea676..06e51c8 100644 --- a/src/browser.rs +++ b/src/browser.rs @@ -1012,7 +1012,13 @@ fn tabs(args: &Value, profile: Option<&str>) -> Result { _ => Ok(if list.is_empty() { "沒有開著的分頁。".into() } else { - list.iter().enumerate().map(|(i, t)| format!("{}. {}{}\n {}", i + 1, t["title"].as_str().unwrap_or(""), if t["id"].as_str() == Some(current.as_str()) { "(目前)" } else { "" }, t["url"].as_str().unwrap_or(""))).collect::>().join("\n") + // Untitled pages (about:blank, bare pages) show their url, so the model never sees an empty line. + list.iter().enumerate().map(|(i, t)| { + let url = t["url"].as_str().unwrap_or(""); + let title = t["title"].as_str().unwrap_or(""); + let title = if title.is_empty() { url } else { title }; + format!("{}. {}{}\n {}", i + 1, title, if t["id"].as_str() == Some(current.as_str()) { "(目前)" } else { "" }, url) + }).collect::>().join("\n") }), } }