← Commit history
CHANGELOG.md+23
@@ -1,5 +1,28 @@ # Changelog — Puppeteer Bridge +## 2.0.442 — 2026-09-06 — thread tiles through the brand raster pipeline, in Rust, on the Adom rounded plate++John: rounded corners like the Adom favicon, two lines at most, no ellipsis, and the downscaling that the+brand analysis chose, embedded with no dependency. The tile is drawn once at 512 px (8x the largest+small entry) on the Adom rounded plate (radius = size / 5.5, the pup icon family's spec; the corners are+transparent) with Node's two-line wrap (whole words when they fit at the floor, else Node's character+split, never an ellipsis), and every ICO entry (16, 20, 24, 32, 40, 48, 64, 256) is produced by the+pipeline gallia's brand skill chose in the May 2026 fifteen-approach shootout: Lanczos3 downscale,+ImageMagick unsharp 0.5x0.5+1.5+0.02, alpha below 25 clamped to 0. All three steps are reimplemented in+pup_identity (plus a dependency-free PNG encoder for the 256 entry); `cargo run -p pup-identity+--example tilepack` is the native harness. Tile generation t9: every cached tile re-renders.++## 2.0.440 — 2026-09-06 — windows placed in CSS pixels again, and placed again once settled (no more overhang on scaled displays)++John, looking at a window on his 150% laptop: "why is that pup window going offscreen with your placement?"+The Rust birth fed the Win32 work area (physical px) to Browser.setWindowBounds (CSS px), so on a scaled+display the window came out DPI-factor too large, overhanging the bottom-right (measured: 2532x1500 at+165,165 on a 2560x1528 work area). Node's v1.8.66 rule is restored: the size is measured from the page+itself (screen.availLeft/Top/Width/Height, CSS px, with retries), 10 px inset; the physical work area+divided by the DPI scale is only the fallback. And the window is placed a second time once it has settled+(Node v1.9.342): Chrome cascades a new window from the previous one after the first move (measured: asked+10,10 CSS, landed 120,120 CSS), and the second placement, verified with Browser.getWindowBounds, sticks.+ ## 2.0.438 — 2026-09-06 — pup_open_window honours `owner`; every thread tile re-rendered  Found while recreating all of John's windows on AdomLapper as proof: pup_open_window ignored the declared
rust/identity/examples/tilepack.rsadded+10
@@ -0,0 +1,10 @@+//! Native harness for the brand tile pipeline: `cargo run -p pup-identity --example tilepack -- <render.rgba> <render_px> <out_dir>`+//! writes <out_dir>/tile-<size>.png for every ICO size (the exact bytes the ICO carries) plus tile.ico.+fn main() {+    let a: Vec<String> = std::env::args().collect();+    let (src, render, out) = (&a[1], a[2].parse::<u32>().unwrap(), &a[3]);+    let rgba = std::fs::read(src).unwrap(); std::fs::create_dir_all(out).unwrap();+    for sz in pup_identity::TILE_SIZES { let px = pup_identity::brand_downscale(&rgba, render, sz); std::fs::write(format!("{out}/tile-{sz}.png"), pup_identity::png_encode_rgba(&px, sz, sz)).unwrap(); }+    std::fs::write(format!("{out}/tile.ico"), pup_identity::ico_from_render(&rgba, render)).unwrap();+    println!("ok");+}
rust/identity/src/lib.rs+111−36
@@ -9,7 +9,7 @@ use std::path::{Path, PathBuf};  pub const APP_ID: &str = "Adom.Pup"; pub const DISPLAY_NAME: &str = "Adom Pup";-pub const TILE_GEN: &str = "t8";+pub const TILE_GEN: &str = "t9"; pub const ICON_GEN: &str = "9"; pub const TITLE_LABEL: &str = "ai-thread"; @@ -78,33 +78,49 @@ pub fn category_icon(cat: Option<&str>, wiki_authed: bool) -> &'static str {  // ── art, rendered by the browser (Node: renderThreadTileIco / dualBadgeB64 / applyAppOverlay) ── +/// The thread-tile canvas script (verbatim port of the Node build's, v2.0.172 sizing rules):+/// resolves to a PNG data URL of a 256x256 teal tile with the name bottom-left. /// The thread-tile canvas script (verbatim port of the Node build's, v2.0.172 sizing rules): /// resolves to a PNG data URL of a 256x256 teal tile with the name bottom-left. pub const TILE_SIZES: [u32; 8] = [16, 20, 24, 32, 40, 48, 64, 256];-/// The thread-tile canvas script (Node v2.0.172 sizing rules). Renders the tile at EVERY ICO size-/// natively (the font engine anti-aliases each size; no downscale), returning JSON-/// {"256": <png data url>, "16": <base64 raw RGBA>, ...}. Node v2.0.160: the taskbar does not render-/// PNG-compressed entries at small sizes, so those are packed as 32-bit DIBs by `ico_from_tiles`.+/// The tile is rendered ONCE at this size (8x the largest DIB entry, per the brand pipeline) and every+/// ICO entry is produced from it by `brand_downscale`.+pub const TILE_RENDER: u32 = 512;+/// The thread-tile canvas script: Node v2.0.172 sizing rules on the Adom rounded plate (corner radius =+/// size / 5.5, the pup icon family's own spec), TWO lines at most (three read too small, John+/// 2026-09-06), names with separators wrapped on whole words when that fits at the floor, else Node's+/// character split; never an ellipsis (every character on a tile is identity). Resolves to base64 of the+/// raw RGBA pixels of the TILE_RENDER canvas; the bridge does the brand downscale for every ICO size. pub fn tile_js(name: &str) -> String {     let nm = serde_json::to_string(name).unwrap_or_else(|_| "\"thread\"".into());     let font = base64_encode(FONT_WOFF2);-    let sizes = serde_json::to_string(&TILE_SIZES).unwrap();+    let render = TILE_RENDER;     format!(r#"(async () => {{   try {{     try {{ const f = new FontFace('FGtile', 'url(data:font/woff2;base64,{font})'); await f.load(); document.fonts.add(f); }} catch (e) {{}}-    const nm = {nm}; const SIZES = {sizes};-    // layout is solved ONCE at 256 (lines + font size as a fraction of the box), then drawn at each size-    const solve = (box) => {{-      const cv = document.createElement('canvas'); cv.width = box; cv.height = box; const ctx = cv.getContext('2d');-      const FLOOR = box * 0.36;-      const wFor = (i, total) => (total === 2 && i === 0) ? box * 0.80 : box * 0.92;-      const setF = (s) => {{ ctx.font = '700 ' + s + "px FGtile,'Segoe UI',sans-serif"; }};-      const wid = (t, s) => {{ setF(s); return ctx.measureText(t).width; }};-      const fitSize = (lines) => {{ let s = box * 0.9; for (let i = 0; i < 90; i++) {{ let ok = true; for (let j = 0; j < lines.length; j++) {{ if (wid(lines[j], s) > wFor(j, lines.length)) {{ ok = false; break; }} }} if (ok) return s; s *= 0.96; }} return s; }};+    const nm = {nm}; const box = 256, R = {render};+    const cv = document.createElement('canvas'); cv.width = box; cv.height = box; const ctx = cv.getContext('2d');+    const setF = (s) => {{ ctx.font = '700 ' + s + "px FGtile,'Segoe UI',sans-serif"; }};+    const wid = (t, s) => {{ setF(s); return ctx.measureText(t).width; }};+    const FLOOR = box * 0.36;+    const wFor = (i, total) => (total === 2 && i === 0) ? box * 0.78 : box * 0.90;   // text starts at 7.5% in; 0.90 keeps the right edge inside the plate (Node: 0.92 from 5.5%)+    const fitSize = (lines) => {{ let s = box * 0.9; for (let i = 0; i < 90; i++) {{ let ok = true; for (let j = 0; j < lines.length; j++) {{ if (wid(lines[j], s) > wFor(j, lines.length)) {{ ok = false; break; }} }} if (ok && lines.length * s * 0.95 <= box * 0.88) break; s *= 0.95; }} return s; }};+    let best = null, bf = 0;+    // whole words first (split on spaces, after hyphens and slashes), two lines, only if it fits at or above the floor+    const toks = nm.split(/\s+/).flatMap(t => t.split(/(?<=[-\/])/)).filter(Boolean);+    if (toks.length > 1) {{+      for (let k = 1; k < toks.length; k++) {{+        const glue = (t) => t.reduce((a, x) => a + ((a && !/[-\/]$/.test(a)) ? ' ' : '') + x, '');+        const c = [glue(toks.slice(0, k)), glue(toks.slice(k))]; const f = fitSize(c);+        if (f >= FLOOR && f > bf) {{ bf = f; best = c; }}+      }}+      const one = fitSize([nm]); if (one >= FLOOR && one > bf) {{ bf = one; best = [nm]; }}+    }}+    if (!best) {{       const cands = [[nm]]; const sp = nm.lastIndexOf(' '), hy = nm.indexOf('-');       if (sp > 0) cands.push([nm.slice(0, sp), nm.slice(sp + 1)]); else if (hy > 0) cands.push([nm.slice(0, hy + 1), nm.slice(hy + 1)]);       if (sp < 0 && hy < 0 && nm.length >= 7) {{ const m = Math.ceil(nm.length / 2); cands.push([nm.slice(0, m), nm.slice(m)]); }}-      let best = [nm], bf = 0;+      best = [nm]; bf = 0;       for (const c of cands) {{ const f = fitSize(c); if (f > bf) {{ bf = f; best = c; }} }}       if (bf < FLOOR) {{         bf = FLOOR;@@ -122,27 +138,16 @@ pub fn tile_js(name: &str) -> String {         }}         best = out.filter(Boolean);       }}-      return {{ lines: best, frac: bf / box }};-    }};-    const L = solve(256);-    const draw = (box) => {{-      const cv = document.createElement('canvas'); cv.width = box; cv.height = box; const ctx = cv.getContext('2d');-      ctx.fillStyle = '#00b8b1'; ctx.fillRect(0, 0, box, box);-      const bf = L.frac * box;-      ctx.font = '700 ' + bf + "px FGtile,'Segoe UI',sans-serif"; ctx.fillStyle = '#05221f'; ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic';-      ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high';-      const lh = bf * 0.95, mx = box * 0.055, baseY = box - box * 0.05 - bf * 0.14;-      L.lines.forEach((ln, i) => ctx.fillText(ln, mx, baseY - (L.lines.length - 1 - i) * lh));-      return cv;-    }};-    const out = {{}};-    for (const s of SIZES) {{-      const cv = draw(s);-      if (s >= 256) {{ out[String(s)] = cv.toDataURL('image/png'); continue; }}-      const d = cv.getContext('2d').getImageData(0, 0, s, s).data; let bin = ''; for (let i = 0; i < d.length; i++) bin += String.fromCharCode(d[i]);-      out[String(s)] = btoa(bin);     }}-    return JSON.stringify(out);+    // draw at R (8x the 64 px entry): rounded plate rx = R/5.5, name bottom-left in the on-accent colour+    const big = document.createElement('canvas'); big.width = R; big.height = R; const c = big.getContext('2d');+    const k = R / box, r = Math.round(R / 5.5);+    c.beginPath(); c.moveTo(r, 0); c.arcTo(R, 0, R, R, r); c.arcTo(R, R, 0, R, r); c.arcTo(0, R, 0, 0, r); c.arcTo(0, 0, R, 0, r); c.closePath(); c.fillStyle = '#00b8b1'; c.fill();+    c.font = '700 ' + (bf * k) + "px FGtile,'Segoe UI',sans-serif"; c.fillStyle = '#05221f'; c.textAlign = 'left'; c.textBaseline = 'alphabetic';+    const lh = bf * k * 0.95, mx = R * 0.075, baseY = R - R * 0.075 - bf * k * 0.14;+    best.forEach((ln, i) => c.fillText(ln, mx, baseY - (best.length - 1 - i) * lh));+    const d = c.getImageData(0, 0, R, R).data; let bin = ''; const CH = 8192; for (let i = 0; i < d.length; i += CH) bin += String.fromCharCode.apply(null, d.subarray(i, Math.min(i + CH, d.length)));+    return JSON.stringify({{ lines: best, size: Math.round(bf), rgba: btoa(bin) }});   }} catch (e) {{ return null; }} }})()"#) }@@ -255,6 +260,76 @@ pub fn badge_js(pct: u32, fallback: Option<&str>) -> String {       .replace("__GENERIC_PDF__", &serde_json::Value::String(generic_icon_svg(true)).to_string()) } +// ── the brand raster pipeline, dependency-free (gallia/brand/logos/SKILL.md, the May 2026 shootout) ──+// resvg at 8x -> PIL Lanczos -> ImageMagick unsharp 0.5x0.5+1.5+0.02 -> alpha < 25 clamped to 0.+// Here the 8x render is the canvas; the three raster steps are reproduced below so the bridge needs no+// image library (John: "embed all that into your rust code so you have no dependencies").++fn lanczos3(x: f64) -> f64 { if x == 0.0 { return 1.0; } let a = 3.0; if x.abs() >= a { return 0.0; } let px = std::f64::consts::PI * x; (a * px.sin() * (px / a).sin()) / (px * px) }++/// PIL's LANCZOS: separable, support 3, the kernel widened by the scale factor when shrinking.+pub fn lanczos_resize(src: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {+    fn weights(src_len: u32, dst_len: u32) -> Vec<(usize, Vec<f64>)> {+        let scale = src_len as f64 / dst_len as f64; let support = 3.0 * scale.max(1.0); let ss = 1.0 / scale.max(1.0);+        (0..dst_len).map(|d| {+            let center = (d as f64 + 0.5) * scale; let lo = ((center - support).floor().max(0.0)) as usize; let hi = ((center + support).ceil().min(src_len as f64)) as usize;+            let mut w: Vec<f64> = (lo..hi).map(|s| lanczos3((s as f64 + 0.5 - center) * ss)).collect(); let sum: f64 = w.iter().sum(); if sum != 0.0 { for v in w.iter_mut() { *v /= sum; } }+            (lo, w)+        }).collect()+    }+    let wx = weights(sw, dw); let wy = weights(sh, dh);+    // horizontal pass -> f64 rows of width dw+    let mut tmp = vec![0f64; (dw * sh * 4) as usize];+    for y in 0..sh as usize { for (dx, (lo, w)) in wx.iter().enumerate() { for ch in 0..4 { let mut acc = 0.0; for (k, wv) in w.iter().enumerate() { acc += *wv * src[(y * sw as usize + lo + k) * 4 + ch] as f64; } tmp[(y * dw as usize + dx) * 4 + ch] = acc; } } }+    let mut out = vec![0u8; (dw * dh * 4) as usize];+    for (dy, (lo, w)) in wy.iter().enumerate() { for x in 0..dw as usize { for ch in 0..4 { let mut acc = 0.0; for (k, wv) in w.iter().enumerate() { acc += *wv * tmp[((lo + k) * dw as usize + x) * 4 + ch]; } out[(dy * dw as usize + x) * 4 + ch] = acc.round().clamp(0.0, 255.0) as u8; } } }+    out+}++/// ImageMagick `-unsharp 0.5x0.5+1.5+0.02` on the colour channels: gaussian blur (sigma 0.5, radius 0.5,+/// which IM widens to a 3x3 kernel), then pixel + 1.5 * (pixel - blur) wherever |pixel - blur| exceeds+/// 2% of full scale.+pub fn unsharp_im(px: &mut [u8], w: u32, h: u32) {+    let (w, h) = (w as usize, h as usize);+    let sigma = 0.5f64; let g = |d: f64| (-(d * d) / (2.0 * sigma * sigma)).exp();+    let k = [g(1.0), g(0.0), g(1.0)]; let ksum: f64 = k.iter().sum();+    let src = px.to_vec();+    let at = |x: isize, y: isize, ch: usize| -> f64 { let xx = x.clamp(0, w as isize - 1) as usize; let yy = y.clamp(0, h as isize - 1) as usize; src[(yy * w + xx) * 4 + ch] as f64 };+    let threshold = 0.02 * 255.0; let gain = 1.5;+    for y in 0..h { for x in 0..w { for ch in 0..3 {+        let mut blur = 0.0; for (j, ky) in k.iter().enumerate() { for (i, kx) in k.iter().enumerate() { blur += ky * kx * at(x as isize + i as isize - 1, y as isize + j as isize - 1, ch); } }+        blur /= ksum * ksum; let orig = src[(y * w + x) * 4 + ch] as f64; let diff = orig - blur;+        if diff.abs() > threshold { px[(y * w + x) * 4 + ch] = (orig + gain * diff).round().clamp(0.0, 255.0) as u8; }+    } } }+}++/// The alpha clamp: alpha below 25 becomes fully transparent (kills the dark fringe on rounded corners).+pub fn alpha_clamp(px: &mut [u8]) { for p in px.chunks_mut(4) { if p[3] < 25 { p[0] = 0; p[1] = 0; p[2] = 0; p[3] = 0; } } }++/// One ICO entry's pixels from the 8x render: Lanczos, unsharp, alpha clamp.+pub fn brand_downscale(src: &[u8], sw: u32, size: u32) -> Vec<u8> { let mut px = lanczos_resize(src, sw, sw, size, size); unsharp_im(&mut px, size, size); alpha_clamp(&mut px); px }++/// Minimal PNG encoder (RGBA, stored deflate blocks) for the 256 entry: no image library needed.+pub fn png_encode_rgba(px: &[u8], w: u32, h: u32) -> Vec<u8> {+    fn crc32(data: &[u8]) -> u32 { let mut c = 0xFFFF_FFFFu32; for &b in data { c ^= b as u32; for _ in 0..8 { c = if c & 1 != 0 { 0xEDB8_8320 ^ (c >> 1) } else { c >> 1 }; } } !c }+    fn chunk(out: &mut Vec<u8>, kind: &[u8], data: &[u8]) { out.extend_from_slice(&(data.len() as u32).to_be_bytes()); let mut cd = kind.to_vec(); cd.extend_from_slice(data); out.extend_from_slice(&cd); out.extend_from_slice(&crc32(&cd).to_be_bytes()); }+    let mut raw = Vec::with_capacity((w * h * 4 + h) as usize); for y in 0..h as usize { raw.push(0); raw.extend_from_slice(&px[y * w as usize * 4..(y + 1) * w as usize * 4]); }+    // zlib stream of stored (uncompressed) deflate blocks+    let mut z = vec![0x78, 0x01]; let mut i = 0; while i < raw.len() { let n = (raw.len() - i).min(65535); let last = i + n >= raw.len(); z.push(if last { 1 } else { 0 }); z.extend_from_slice(&(n as u16).to_le_bytes()); z.extend_from_slice(&(!(n as u16)).to_le_bytes()); z.extend_from_slice(&raw[i..i + n]); i += n; }+    let (mut a, mut b) = (1u32, 0u32); for &byte in &raw { a = (a + byte as u32) % 65521; b = (b + a) % 65521; } z.extend_from_slice(&((b << 16) | a).to_be_bytes());+    let mut out = b"\x89PNG\r\n\x1a\n".to_vec();+    let mut ihdr = Vec::new(); ihdr.extend_from_slice(&w.to_be_bytes()); ihdr.extend_from_slice(&h.to_be_bytes()); ihdr.extend_from_slice(&[8, 6, 0, 0, 0]);+    chunk(&mut out, b"IHDR", &ihdr); chunk(&mut out, b"IDAT", &z); chunk(&mut out, b"IEND", &[]); out+}++/// The whole tile ICO from the 8x render: DIB entries for every small size, PNG for 256.+pub fn ico_from_render(rgba: &[u8], render: u32) -> Vec<u8> {+    let mut raw = Vec::new();+    for sz in TILE_SIZES { if sz >= 256 { continue; } raw.push((sz, brand_downscale(rgba, render, sz))); }+    let big = brand_downscale(rgba, render, 256);+    ico_from_tiles(&raw, &png_encode_rgba(&big, 256, 256))+}+ /// A single-image ICO wrapping PNG bytes (Vista+ format; Windows AUMID registry icons accept it). pub fn png_to_ico(png: &[u8], width: u32, height: u32) -> Vec<u8> {     let mut out = Vec::with_capacity(png.len() + 22);
rust/verbs/src/identity.rs+8−6
@@ -63,12 +63,14 @@ fn ensure_tile(ctx: &Ctx, sid: &str, name: &str) -> Option<PathBuf> {     let path = dir.join(format!("{}.ico", id::thread_slug(name)));     if std::fs::metadata(&path).map(|m| m.len() > 100).unwrap_or(false) { return Some(path); }     let s = ctx.sessions.get(sid)?; let b = browser_for(ctx, &s.profile)?; let t = s.active()?.clone();-    let v = b.cdp.evaluate(&t.cdp_session, &id::tile_js(name), Duration::from_secs(15)).ok()?;-    let m: serde_json::Map<String, Value> = serde_json::from_str(v.as_str()?).ok()?;-    let png = pup_cdp::base64_decode(m.get("256")?.as_str()?.split("base64,").nth(1)?).ok()?;-    let mut raw = Vec::new();-    for sz in id::TILE_SIZES { if sz >= 256 { continue; } if let Some(b) = m.get(&sz.to_string()).and_then(|x| x.as_str()) { if let Ok(bytes) = pup_cdp::base64_decode(b) { raw.push((sz, bytes)); } } }-    std::fs::write(&path, id::ico_from_tiles(&raw, &png)).ok()?;+    // The page renders the tile ONCE at 8x (TILE_RENDER); every ICO entry comes out of the brand raster+    // pipeline in pup_identity (Lanczos3, ImageMagick unsharp 0.5x0.5+1.5+0.02, alpha clamp), no image library.+    let v = b.cdp.evaluate(&t.cdp_session, &id::tile_js(name), Duration::from_secs(30)).ok()?;+    let m: Value = serde_json::from_str(v.as_str()?).ok()?;+    let rgba = pup_cdp::base64_decode(m["rgba"].as_str()?).ok()?;+    if rgba.len() != (id::TILE_RENDER * id::TILE_RENDER * 4) as usize { pup_core::log(format!("[thread-tile] \"{name}\" render returned {} bytes, expected {}", rgba.len(), id::TILE_RENDER * id::TILE_RENDER * 4)); return None; }+    std::fs::write(&path, id::ico_from_render(&rgba, id::TILE_RENDER)).ok()?;+    pup_core::log(format!("[thread-tile] \"{name}\" lines {} at {}px, brand pipeline", m["lines"], m["size"]));     pup_core::log(format!("[thread-tile] rendered tile for \"{name}\" -> {}", path.display()));     Some(path) }
rust/verbs/src/lib.rs+24−4
@@ -625,8 +625,19 @@ fn my_windows(ctx: &Ctx, caller: &Caller) -> Value {     r } -fn birth_rect(ctx: &Ctx) -> (i32, i32, i32, i32) {-    match ctx.host.work_area() { Some((x, y, w, h)) => (x + 10, y + 10, (w - 20).max(400), (h - 20).max(300)), None => (10, 10, 1280, 800) }+/// The park geometry in CSS px (what Browser.setWindowBounds takes): Node v1.8.66 rule, "size is+/// measured, not guessed": screen.availLeft/Top/Width/Height from the page itself, with retries, then a+/// 10 px inset on every side. The Win32 work area is PHYSICAL px; feeding it to CDP on a 150% display+/// produced a window 1.5x the screen, hanging off the bottom-right (John's AdomLapper, 2026-09-06).+fn birth_rect_css(cdp: &Cdp, cdp_session: &str, ctx: &Ctx) -> (i32, i32, i32, i32) {+    for _ in 0..4 {+        if let Ok(v) = cdp.evaluate(cdp_session, "({x:screen.availLeft||0,y:screen.availTop||0,w:screen.availWidth,h:screen.availHeight})", Duration::from_secs(3)) {+            let (x, y, w, h) = (v["x"].as_f64().unwrap_or(0.0) as i32, v["y"].as_f64().unwrap_or(0.0) as i32, v["w"].as_f64().unwrap_or(0.0) as i32, v["h"].as_f64().unwrap_or(0.0) as i32);+            if w > 400 && h > 300 { return (x + 10, y + 10, w - 20, h - 20); }+        }+        std::thread::sleep(Duration::from_millis(300));+    }+    match ctx.host.work_area() { Some((x, y, w, h)) => { let sc = ctx.host.dpi_scale(0).max(1.0); ((x as f64 / sc) as i32 + 10, (y as f64 / sc) as i32 + 10, ((w as f64 / sc) as i32 - 20).max(400), ((h as f64 / sc) as i32 - 20).max(300)) }, None => (10, 10, 1280, 800) } }  fn open_window(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {@@ -664,10 +675,9 @@ fn open_window(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {         }     } -    let (bl, bt, bw, bh) = birth_rect(ctx);     let before: std::collections::HashSet<i64> = ctx.host.windows_for_pids(&[], Some("Chrome_WidgetWin_1")).into_iter().map(|w| w.hwnd).collect();     let prev_fg = ctx.host.foreground();   // Node v1.9.253: measure whether creation steals focus, and hand it straight back-    let target = match b.cdp.create_target("about:blank", true, Some((OFFSCREEN, OFFSCREEN, bw, bh))) { Ok(t) => t, Err(e) => return err("cdp_create_target_failed", format!("Target.createTarget: {e}"), Some("The browser is up but refused a new window; pup_readiness shows its state, pup_close {} restarts it.")) };+    let target = match b.cdp.create_target("about:blank", true, Some((OFFSCREEN, OFFSCREEN, 1280, 800))) { Ok(t) => t, Err(e) => return err("cdp_create_target_failed", format!("Target.createTarget: {e}"), Some("The browser is up but refused a new window; pup_readiness shows its state, pup_close {} restarts it.")) };     // Node: an explicit `owner` declares the owning thread (ownerSource "declared"); else the caller's thread.     // The Rust port ignored `owner` until 2.0.438, so a window recreated on another thread's behalf wore the     // recreating thread's tile (John's AdomLapper proof run, 2026-09-06).@@ -676,6 +686,7 @@ fn open_window(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {     let mut s = Session::new(&sid, &profile, owner, Some(&caller.container), b.launched.cdp_port, if b.launched.pid > 0 { Some(b.launched.pid) } else { None });     s.owner_from_caller = s.owner.as_deref() == Some(caller.thread.as_str()) && !caller.thread.is_empty() && args.get("owner").is_none();     let tab_id = match attach_tab(&mut s, &b.cdp, &target) { Ok(t) => t, Err(e) => return e };+    let (bl, bt, bw, bh) = s.tabs.last().map(|t| birth_rect_css(&b.cdp, &t.cdp_session, ctx)).unwrap_or((10, 10, 1280, 800));   // CSS px, measured from the page     // park: on-screen rect via CDP (moves without z-order change), then bottom + tag via the host     let mut hwnd: Option<i64> = None;     // Node v2.0.276: born off-screen so Chrome never activates it, moved on-screen within the 50 ms budget by@@ -729,6 +740,15 @@ fn open_window(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {         // first real paint (this navigation) tries to activate the window and is denied. No button existed         // (DeleteTab at birth), so nothing was flagged. Now stamp the thread identity and let the shell         // create the one button this window will ever have, wearing the tile.+        // Node v1.9.342: place AGAIN once the window has settled. Chrome cascades a new window from the+        // last one's position after our first move (measured on AdomLapper: requested 10,10 CSS, landed+        // 120,120 CSS, overhanging a 150% screen), and re-reads the screen the window actually sits on.+        if let (Some(t), Ok((wid, _))) = (s.tabs.last(), b.cdp.window_for_target(&target)) {+            let (l2, t2, w2, h2) = birth_rect_css(&b.cdp, &t.cdp_session, ctx);+            let _ = b.cdp.set_window_bounds(wid, json!({ "windowState": "normal" }));+            let _ = b.cdp.set_window_bounds(wid, json!({ "left": l2, "top": t2, "width": w2, "height": h2 }));+            if let Ok(v) = b.cdp.send(None, "Browser.getWindowBounds", json!({ "windowId": wid }), Duration::from_secs(3)) { pup_core::log(format!("[window] \"{sid}\" placed at {},{} {}x{} CSS (asked {l2},{t2} {w2}x{h2})", v["bounds"]["left"], v["bounds"]["top"], v["bounds"]["width"], v["bounds"]["height"])); }+        }         let t_id = Instant::now();         identity::stamp_opts(ctx, &sid, caller, true, false);         let _ = ctx.host.taskbar_tab(h, false);