← Commit history

v2.0.704 (insiders): Annotate works again on Gmail, Google Docs, YouTube and every page that enforces Trusted Types: 2.0.701's new hint line was written as an HTML string, those pages refuse that, and the whole toolbar failed to start (John's Gmail tab, 2026-10-07). The hint and the corner pencil are built from DOM nodes, a test now fails the build if either script assigns HTML strings again, and an overlay that cannot start is logged with its reason. Gates: smoke all pass and parity 55/56 on arav-rog; foreground and jump list not run (winvm offline, other threads' windows on the test boxes), promoted on John's go.

John Lauer ·e622599515 ·1mo ago ·parent b1a4eac
5 files changed +46−23
rust/Cargo.lock+12−12
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-pup-bridge"-version = "2.0.703"+version = "2.0.704" dependencies = [  "pup-browser",  "pup-cdp",@@ -655,7 +655,7 @@ dependencies = [  [[package]] name = "pup-browser"-version = "2.0.703"+version = "2.0.704" dependencies = [  "pup-core",  "pup-host",@@ -667,7 +667,7 @@ dependencies = [  [[package]] name = "pup-cdp"-version = "2.0.703"+version = "2.0.704" dependencies = [  "serde_json",  "thiserror",@@ -677,7 +677,7 @@ dependencies = [  [[package]] name = "pup-core"-version = "2.0.703"+version = "2.0.704" dependencies = [  "serde",  "serde_json",@@ -687,7 +687,7 @@ dependencies = [  [[package]] name = "pup-dashboard"-version = "2.0.703"+version = "2.0.704" dependencies = [  "pup-cdp",  "pup-core",@@ -700,7 +700,7 @@ dependencies = [  [[package]] name = "pup-host"-version = "2.0.703"+version = "2.0.704" dependencies = [  "serde",  "serde_json",@@ -709,14 +709,14 @@ dependencies = [  [[package]] name = "pup-host-mac"-version = "2.0.703"+version = "2.0.704" dependencies = [  "pup-host", ]  [[package]] name = "pup-host-win"-version = "2.0.703"+version = "2.0.704" dependencies = [  "pup-core",  "pup-host",@@ -726,7 +726,7 @@ dependencies = [  [[package]] name = "pup-identity"-version = "2.0.703"+version = "2.0.704" dependencies = [  "fontdue",  "image",@@ -736,7 +736,7 @@ dependencies = [  [[package]] name = "pup-session"-version = "2.0.703"+version = "2.0.704" dependencies = [  "pup-core",  "serde",@@ -746,7 +746,7 @@ dependencies = [  [[package]] name = "pup-vault"-version = "2.0.703"+version = "2.0.704" dependencies = [  "pup-core",  "pup-host",@@ -757,7 +757,7 @@ dependencies = [  [[package]] name = "pup-verbs"-version = "2.0.703"+version = "2.0.704" 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.703"+version = "2.0.704" edition = "2021" license = "MIT" 
rust/verbs/assets/floaty.js+10−4
@@ -29,11 +29,17 @@     var btn = document.createElement('button');     btn.setAttribute('aria-label', 'Annotate this page');     btn.style.cssText = 'width:30px;height:30px;border-radius:8px;display:grid;place-items:center;background:rgba(13,17,23,.72);border:1px solid rgba(0,184,177,.45);cursor:pointer;padding:0;';-    btn.innerHTML = '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="#e6edf3" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20l1.5-4.5L16 5a2.1 2.1 0 0 1 3 3L8.5 18.5 4 20z"/><path d="M13.5 7.5l3 3"/></svg>';+    // 2.0.704: nodes, never innerHTML (Trusted Types pages such as Gmail refuse HTML strings and the throw kills the script).+    var NS = 'http://www.w3.org/2000/svg', svg = document.createElementNS(NS, 'svg');+    [['viewBox','0 0 24 24'],['width','15'],['height','15'],['fill','none'],['stroke','#e6edf3'],['stroke-width','1.8'],['stroke-linecap','round'],['stroke-linejoin','round']].forEach(function (a) { svg.setAttribute(a[0], a[1]); });+    ['M4 20l1.5-4.5L16 5a2.1 2.1 0 0 1 3 3L8.5 18.5 4 20z', 'M13.5 7.5l3 3'].forEach(function (d) { var p = document.createElementNS(NS, 'path'); p.setAttribute('d', d); svg.appendChild(p); });+    btn.appendChild(svg);     var tip = document.createElement('div');-    tip.innerHTML = '<div style="font-weight:700;color:#e6edf3;margin-bottom:4px;font-family:Familjen Grotesk,Satoshi,sans-serif;">Annotate this page</div>'-      + '<div style="color:#c9d1d9;">Draw on the page to show your AI exactly what you mean; a red circle says it best. The moment you lift the mouse, the marked-up area is copied to your clipboard, ready to paste into any AI chat.</div>'-      + '<div style="margin-top:7px;color:#8b949e;font-size:11.5px;">Shortcut&nbsp;&nbsp;<span style="font-family:JetBrains Mono,monospace;font-size:10.5px;border:1px solid #30363d;border-radius:4px;padding:1px 6px;color:#c9d1d9;">Alt&nbsp;Shift&nbsp;A</span></div>';+    var t1 = document.createElement('div'); t1.style.cssText = 'font-weight:700;color:#e6edf3;margin-bottom:4px;font-family:Familjen Grotesk,Satoshi,sans-serif;'; t1.textContent = 'Annotate this page';+    var t2 = document.createElement('div'); t2.style.cssText = 'color:#c9d1d9;'; t2.textContent = 'Draw on the page to show your AI exactly what you mean; a red circle says it best. Press Enter to copy the marked-up area to your clipboard, ready to paste into any AI chat.';+    var t3 = document.createElement('div'); t3.style.cssText = 'margin-top:7px;color:#8b949e;font-size:11.5px;'; t3.textContent = 'Shortcut\u00a0\u00a0';+    var kb = document.createElement('span'); kb.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:10.5px;border:1px solid #30363d;border-radius:4px;padding:1px 6px;color:#c9d1d9;'; kb.textContent = 'Alt\u00a0Shift\u00a0A'; t3.appendChild(kb);+    tip.appendChild(t1); tip.appendChild(t2); tip.appendChild(t3);     tip.style.cssText = 'position:fixed;z-index:2147483647;max-width:min(320px,calc(100vw - 40px));background:rgba(22,27,34,.96);color:#e6edf3;border:1px solid rgba(0,184,177,.28);border-radius:10px;padding:9px 13px;font:400 13px/1.4 Satoshi,system-ui,sans-serif;box-shadow:0 12px 40px rgba(0,0,0,.55);backdrop-filter:blur(16px);pointer-events:none;opacity:0;transform:translateY(4px);transition:opacity .18s ease,transform .18s ease;';     row.appendChild(grip); row.appendChild(btn); root.appendChild(row); root.appendChild(tip);     var timer = null, pinned = false, shown = false, hideTimer = null, idleTimer = null, moves = 0;
rust/verbs/src/annotate.rs+11−2
@@ -29,9 +29,9 @@ pub fn start(ctx: &Ctx, caller: &Caller, args: &Value) -> Value {     // 2.0.569: both lanes. The overlay is one file in the extension folder: file-injected on the extension     // lane (a page CSP cannot refuse that, unlike eval; Google Docs and the wiki used to fail here), and     // evaluated over CDP on the CDP lane.-    if let Err(e) = crate::transport::inject_script(&lane, &t, "annotate-overlay.js", OVERLAY_JS, "window.__pupStartAnnotate") { return pup_core::merge(e, json!({ "errorCode": "annotate_inject_failed" })); }+    if let Err(e) = crate::transport::inject_script(&lane, &t, "annotate-overlay.js", OVERLAY_JS, "window.__pupStartAnnotate") { pup_core::log(format!("[annotate] \"{}\" could not inject the overlay: {}", s.id, e["error"].as_str().unwrap_or("?"))); return pup_core::merge(e, json!({ "errorCode": "annotate_inject_failed" })); }     let mode_arg = mode.as_ref().map(|m| json!(m)).unwrap_or(Value::Null);-    if let Err(e) = crate::transport::call_window_fn(&lane, &t, "__pupStartAnnotate", &[json!(tool), mode_arg]) { return pup_core::merge(e, json!({ "errorCode": "annotate_start_failed" })); }+    if let Err(e) = crate::transport::call_window_fn(&lane, &t, "__pupStartAnnotate", &[json!(tool), mode_arg]) { pup_core::log(format!("[annotate] \"{}\" the overlay failed to start in the page: {}", s.id, e["error"].as_str().unwrap_or("?"))); return pup_core::merge(e, json!({ "errorCode": "annotate_start_failed" })); }     pup_core::log(format!("[annotate] \"{}\" overlay started (by {}, tool={tool}, mode={}, lane={})", s.id, caller.thread, mode.as_deref().unwrap_or("default"), if crate::transport::is_ext(&s) { "ext" } else { "cdp" }));     if let Some(a) = ctx.arc() { watch(a, s.id.clone(), caller.clone()); }     touch(ctx, &s.id, None);@@ -125,5 +125,14 @@ pub fn floaty_click(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { let mut #[cfg(test)] mod tests {     use super::*;+    // 2.0.704: Trusted Types pages (Gmail, Google Docs) throw on any HTML-string assignment and the overlay dies with+    // it. Both in-page scripts must build their DOM from nodes; this fails the build if either one regresses.+    #[test] fn no_html_string_sinks() {+        for (name, src) in [("annotate-overlay.js", OVERLAY_JS), ("floaty.js", FLOATY_JS)] {+            for sink in [".innerHTML =", ".innerHTML=", ".outerHTML", "insertAdjacentHTML", "document.write("] {+                assert!(!src.contains(sink), "{name} uses {sink}: Trusted Types pages (Gmail, Google Docs) refuse it and the overlay dies");+            }+        }+    }     #[test] fn itxt() { assert_eq!(crc32(b"123456789"), 0xCBF43926); let png = b"\x89PNG\r\n\x1a\n\0\0\0\0IEND\xaeB`\x82".to_vec(); let out = png_add_itxt(&png, "k", "v"); assert!(out.windows(4).any(|w| w == b"iTXt")); assert!(out.ends_with(b"IEND\xaeB`\x82")); } }
src/extension/annotate-overlay.js+12−4
@@ -270,13 +270,21 @@ window.__pupStartAnnotate = function (initTool, initMode) {   var hint = document.createElement('div');   hint.style.cssText = 'position:absolute;top:100%;left:50%;transform:translateX(-50%);margin-top:6px;white-space:nowrap;background:#161b22e6;border:1px solid #30363d;border-radius:999px;padding:3px 11px;font:12px Satoshi,system-ui,sans-serif;color:#8b949e;pointer-events:none;';   bar.style.position = bar.style.position || 'fixed'; bar.appendChild(hint);-  function kbd(k) { return '<b style="color:#c9d1d9;font-weight:600">' + k + '</b>'; }+  // 2.0.704: built from nodes, never innerHTML. Gmail, Google Docs and every other Trusted Types page refuse an HTML+  // string, the throw killed the whole overlay, and the pen did nothing there (John's Gmail tab, 2026-10-07).+  function setHint(parts) {+    while (hint.firstChild) hint.removeChild(hint.firstChild);+    parts.forEach(function (p) {+      if (p.k) { var b = document.createElement('b'); b.style.cssText = 'color:#c9d1d9;font-weight:600'; b.textContent = p.k; hint.appendChild(b); }+      else hint.appendChild(document.createTextNode(p));+    });+  }   function paintMode() {     mAuto.style.background = S.quick ? 'rgba(0,184,177,.13)' : 'none'; mAuto.style.color = S.quick ? '#00d4cb' : '#c9d1d9';     mMulti.style.background = !S.quick ? 'rgba(0,184,177,.13)' : 'none'; mMulti.style.color = !S.quick ? '#00d4cb' : '#c9d1d9';-    hint.innerHTML = S.quick-      ? 'Single: lift the mouse to capture \u00b7 ' + kbd('Space') + ' for Multi \u00b7 ' + kbd('Esc') + ' to cancel'-      : 'Multi: ' + kbd('Enter') + ' to finish \u00b7 ' + kbd('Space') + ' for Single \u00b7 ' + kbd('Esc') + ' to cancel';+    setHint(S.quick+      ? ['Single: lift the mouse to capture \u00b7 ', { k: 'Space' }, ' for Multi \u00b7 ', { k: 'Esc' }, ' to cancel']+      : ['Multi: ', { k: 'Enter' }, ' to finish \u00b7 ', { k: 'Space' }, ' for Single \u00b7 ', { k: 'Esc' }, ' to cancel']);   }   paintMode(); bar.appendChild(modeWrap);   btn('Undo', 'Remove the last thing you drew. Click again to keep unwinding.', function () { S.strokes.pop(); redraw(); }, null, 'undo');