← Commit history

v2.0.569 (insiders): Annotate on the extension lane; worker self-reload after an install; overlay badge and thread tile in the binary; ext windows branded before first show.

John Lauer ·858c627ce6 ·1mo ago ·parent 5fea507
2 files changed +364
src/extension/annotate-overlay.jsadded+335
@@ -0,0 +1,335 @@+window.__pupStartAnnotate = function (initTool, initMode) {+  if (window.__pupAnnotate) {+    // v2.0.372: a re-arm with an explicit mode SWITCHES the live overlay's mode (Alt+Shift+M on+    // an already-open toolbar). v2.0.81 rule preserved: a re-arm after emit-hide stays INVISIBLE.+    if (initMode && window.__pupAnnotate.setMode) { try { window.__pupAnnotate.setMode(initMode); } catch (e) {} }+    if (!window.__pupAnnotateHidden) window.__pupAnnotate.show();+    return 'shown';+  }+  initTool = initTool || 'pen';+  // v2.0.370 (John): two explicit capture modes. quick=true (Auto, the default) captures the+  // moment the mouse lifts — the one-red-circle case. quick=false (Multi) accumulates shapes —+  // a red circle here, a blue circle there — and captures ONCE on Done.+  var S = { tool: 'pen', color: '#f85149', strokes: [], cur: null, quick: initMode !== 'multi', seq: 0 };+  var wrap = document.createElement('div');+  wrap.id = '__pup_annotate';+  wrap.style.cssText = 'position:fixed;inset:0;z-index:2147483646;pointer-events:none;';+  var cv = document.createElement('canvas');+  cv.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;pointer-events:auto;touch-action:none;';+  wrap.appendChild(cv);+  var bar = document.createElement('div');+  bar.style.cssText = 'position:fixed;top:14px;left:50%;transform:translateX(-50%);display:flex;gap:6px;align-items:center;background:#161b22f2;border:1px solid #30363d;border-radius:999px;padding:7px 12px;pointer-events:auto;font:13px Satoshi,system-ui,sans-serif;color:#c9d1d9;box-shadow:0 6px 24px #000a;';+  wrap.appendChild(bar);+  // drag grip (floating-windows: draggable by a visible grip, clamped inside the viewport)+  var grip = document.createElement('div');+  grip.title = '';+  grip.style.cssText = 'width:14px;height:22px;cursor:grab;display:grid;grid-template-columns:1fr 1fr;gap:2px;align-content:center;justify-items:center;margin-right:2px;';+  for (var gi = 0; gi < 6; gi++) { var gd = document.createElement('div'); gd.style.cssText = 'width:3px;height:3px;border-radius:50%;background:#8b949e;'; grip.appendChild(gd); }+  bar.appendChild(grip);+  (function () {+    var drag = null;+    grip.addEventListener('pointerdown', function (e) {+      var r = bar.getBoundingClientRect();+      if (bar.style.transform) { bar.style.transform = ''; bar.style.left = r.left + 'px'; bar.style.top = r.top + 'px'; }+      drag = { dx: e.clientX - r.left, dy: e.clientY - r.top };+      grip.setPointerCapture(e.pointerId); grip.style.cursor = 'grabbing'; e.preventDefault();+    });+    grip.addEventListener('pointermove', function (e) {+      if (!drag) return;+      var w = bar.offsetWidth, h = bar.offsetHeight, M = 8;+      var x = Math.min(Math.max(M, e.clientX - drag.dx), innerWidth - w - M);+      var y = Math.min(Math.max(M, e.clientY - drag.dy), innerHeight - h - M);+      bar.style.left = x + 'px'; bar.style.top = y + 'px';+    });+    grip.addEventListener('pointerup', function () { drag = null; grip.style.cursor = 'grab'; });+  })();+  // tooltip singleton (tooltips skill: fixed, body-level z-max, 600ms reveal, 0.18s ease+  // fade+translate, measured AFTER content set, clamped to the viewport on BOTH axes, flips+  // above when it would clip the bottom)+  var tipEl = document.createElement('div');+  tipEl.style.cssText = 'position:fixed;z-index:2147483647;max-width:280px;background:#21262d;border:1px solid #30363d;border-radius:8px;padding:7px 10px;font:12px Satoshi,system-ui,sans-serif;color:#c9d1d9;box-shadow:0 6px 20px #000b;pointer-events:none;opacity:0;transform:translateY(4px);transition:opacity 0.18s ease,transform 0.18s ease;text-transform:none;';+  wrap.appendChild(tipEl);+  var tipTimer = null;+  function tipShow(anchor, text) {+    clearTimeout(tipTimer);+    tipTimer = setTimeout(function () {+      tipEl.textContent = text;+      tipEl.style.opacity = '0';+      // measure only after content is set and laid out+      requestAnimationFrame(function () {+        var a = anchor.getBoundingClientRect(), M = 8;+        var w = tipEl.offsetWidth, h = tipEl.offsetHeight;+        var x = a.left + a.width / 2 - w / 2;+        var y = a.bottom + 10;+        if (y + h > innerHeight - M) y = a.top - h - 10;   // flip above instead of clipping+        x = Math.min(Math.max(M, x), innerWidth - w - M);+        y = Math.min(Math.max(M, y), innerHeight - h - M);+        tipEl.style.left = x + 'px'; tipEl.style.top = y + 'px';+        tipEl.style.opacity = '1'; tipEl.style.transform = 'translateY(0)';+      });+    }, 600);+  }+  function tipHide() { clearTimeout(tipTimer); tipEl.style.opacity = '0'; tipEl.style.transform = 'translateY(4px)'; }+  function tipFor(el, text) { el.addEventListener('mouseenter', function () { tipShow(el, text); }); el.addEventListener('mouseleave', tipHide); el.addEventListener('pointerdown', tipHide); el.removeAttribute('title'); }+  tipFor(grip, 'Drag to move this toolbar anywhere it is not in your way.');+  var ctx = cv.getContext('2d');+  // ── Photoshop-style tool cursors (v2.0.134, John's HN report: "pen mode shows the crosshair,+  // must be old code"): it was NOT old code. The pen/arrow cursors were CSS+  // url(data:image/svg+xml) images, and CSS cursor images load under the PAGE'S CSP img-src —+  // sites that don't allow data: (Hacker News: img-src 'self' ...) silently block the image and+  // CSS falls back to the declared crosshair, so one build looked new on the wiki and old on HN.+  // Fix: never FETCH a cursor at all. Hide the native cursor over the canvas and follow the+  // pointer with an inline-SVG sprite — DOM markup, which no CSP gates — hotspot preserved+  // (pen tip at 5,19; arrow tip at 2,2). Pen still carries the live color swatch.+  var spr = document.createElement('div');+  spr.style.cssText = 'position:fixed;left:0;top:0;z-index:2147483647;pointer-events:none;display:none;will-change:transform;';+  wrap.appendChild(spr);+  var sprHot = [0, 0];+  function penSvg() {+    return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">'+      + '<path d="M5 19 L6.5 14.5 L16 5 a2 2 0 0 1 2.8 2.8 L9.5 17.5 Z" fill="#e5e7eb" stroke="#111" stroke-width="1.2"/>'+      + '<path d="M5 19 L6.5 14.5 L9.5 17.5 Z" fill="' + S.color + '" stroke="#111" stroke-width="0.8"/>'+      + '<circle cx="19.5" cy="19.5" r="3.5" fill="' + S.color + '" stroke="#fff" stroke-width="1"/></svg>';+  }+  function arrowSvg() {+    return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">'+      + '<path d="M2 2 L11 20 L13.5 13.5 L20 11 Z" fill="' + S.color + '" stroke="#fff" stroke-width="1.4"/></svg>';+  }+  function setCursor() {+    if (S.tool === 'pen' || S.tool === 'arrow') {+      cv.style.cursor = 'none';+      spr.innerHTML = S.tool === 'pen' ? penSvg() : arrowSvg();+      sprHot = S.tool === 'pen' ? [5, 19] : [2, 2];+      // shown on the next pointer move, so it never appears at a stale position+    } else {+      cv.style.cursor = S.tool === 'text' ? 'text' : 'crosshair';+      spr.style.display = 'none';+    }+  }+  function sprTrack(e) {+    if (S.tool !== 'pen' && S.tool !== 'arrow') return;+    spr.style.display = 'block';+    spr.style.transform = 'translate(' + (e.clientX - sprHot[0]) + 'px,' + (e.clientY - sprHot[1]) + 'px)';+  }+  cv.addEventListener('pointermove', sprTrack);+  cv.addEventListener('pointerenter', sprTrack);+  cv.addEventListener('pointerleave', function () { spr.style.display = 'none'; });+  function fit() { var d = window.devicePixelRatio || 1; cv.width = innerWidth * d; cv.height = innerHeight * d; ctx.setTransform(d, 0, 0, d, 0, 0); redraw(); }+  function drawStroke(st) {+    ctx.strokeStyle = st.color; ctx.fillStyle = st.color; ctx.lineWidth = 3.5; ctx.lineCap = 'round'; ctx.lineJoin = 'round';+    if (st.tool === 'pen') { ctx.beginPath(); for (var i = 0; i < st.pts.length; i++) { var p = st.pts[i]; if (i === 0) ctx.moveTo(p[0], p[1]); else ctx.lineTo(p[0], p[1]); } ctx.stroke(); }+    else if (st.tool === 'box') { var a = st.pts[0], b = st.pts[st.pts.length - 1]; ctx.strokeRect(Math.min(a[0],b[0]), Math.min(a[1],b[1]), Math.abs(b[0]-a[0]), Math.abs(b[1]-a[1])); }+    else if (st.tool === 'arrow') { var a2 = st.pts[0], b2 = st.pts[st.pts.length - 1]; ctx.beginPath(); ctx.moveTo(a2[0], a2[1]); ctx.lineTo(b2[0], b2[1]); ctx.stroke();+      var ang = Math.atan2(b2[1]-a2[1], b2[0]-a2[0]);+      ctx.beginPath(); ctx.moveTo(b2[0], b2[1]);+      ctx.lineTo(b2[0]-14*Math.cos(ang-0.42), b2[1]-14*Math.sin(ang-0.42));+      ctx.lineTo(b2[0]-14*Math.cos(ang+0.42), b2[1]-14*Math.sin(ang+0.42));+      ctx.closePath(); ctx.fill(); }+    else if (st.tool === 'text') { ctx.font = '600 17px system-ui,sans-serif'; ctx.fillText(st.text || '', st.pts[0][0], st.pts[0][1]); }+    else if (st.tool === 'snip') { var s1 = st.pts[0], s2 = st.pts[st.pts.length - 1]; ctx.save(); ctx.setLineDash([7, 5]); ctx.lineWidth = 2; ctx.strokeStyle = '#00d4cb'; ctx.strokeRect(Math.min(s1[0],s2[0]), Math.min(s1[1],s2[1]), Math.abs(s2[0]-s1[0]), Math.abs(s2[1]-s1[1])); ctx.restore(); }+  }+  function redraw() { ctx.clearRect(0, 0, innerWidth, innerHeight); S.strokes.forEach(drawStroke); if (S.cur) drawStroke(S.cur); }+  // Tight-crop bbox over ALL strokes, padded and clamped to the viewport (1a2).+  function bbox() {+    if (!S.strokes.length) return null;+    var x1 = 1e9, y1 = 1e9, x2 = -1e9, y2 = -1e9;+    S.strokes.forEach(function (st) {+      if (st.tool === 'text') {+        var w = (st.text || '').length * 9.5, p0 = st.pts[0];+        x1 = Math.min(x1, p0[0]); y1 = Math.min(y1, p0[1] - 18); x2 = Math.max(x2, p0[0] + w); y2 = Math.max(y2, p0[1] + 6);+      } else st.pts.forEach(function (p) { x1 = Math.min(x1, p[0]); y1 = Math.min(y1, p[1]); x2 = Math.max(x2, p[0]); y2 = Math.max(y2, p[1]); });+    });+    var M = 48;+    x1 = Math.max(0, x1 - M); y1 = Math.max(0, y1 - M);+    x2 = Math.min(innerWidth, x2 + M); y2 = Math.min(innerHeight, y2 + M);+    if (x2 - x1 < 24 || y2 - y1 < 24) return null;+    return { x: Math.round(x1), y: Math.round(y1), w: Math.round(x2 - x1), h: Math.round(y2 - y1) };+  }+  function emit(extra, hide) {+    S.seq++;+    var r = { done: true, shapes: S.strokes.length, ts: Date.now(), seq: S.seq, bbox: bbox() };+    if (extra) for (var k in extra) r[k] = extra[k];+    if (hide) { bar.style.display = 'none'; cv.style.pointerEvents = 'none'; window.__pupAnnotateHidden = true; }+    window.__pupAnnotateResult = r;+  }+  cv.addEventListener('pointerdown', function (e) {+    if (S.tool === 'text') { var t = prompt('Annotation text:'); if (t) { S.strokes.push({ tool: 'text', color: S.color, pts: [[e.clientX, e.clientY]], text: t }); redraw(); if (S.quick) { emit(null, true); removeEventListener('keydown', onKey, true); } else advanceColor(); } return; }+    S.cur = { tool: S.tool, color: S.color, pts: [[e.clientX, e.clientY]] }; cv.setPointerCapture(e.pointerId);+  });+  cv.addEventListener('pointermove', function (e) { if (S.cur) { S.cur.pts.push([e.clientX, e.clientY]); redraw(); } });+  cv.addEventListener('pointerup', function () {+    if (!S.cur) return;+    if (S.cur.tool === 'snip') {+      var p1 = S.cur.pts[0], p2 = S.cur.pts[S.cur.pts.length - 1];+      var r = { x: Math.min(p1[0], p2[0]), y: Math.min(p1[1], p2[1]), w: Math.abs(p2[0]-p1[0]), h: Math.abs(p2[1]-p1[1]) };+      S.cur = null; redraw();+      if (r.w > 8 && r.h > 8) emit({ snip: { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.w), h: Math.round(r.h) } }, true);+      return;+    }+    S.strokes.push(S.cur); S.cur = null; redraw();+    // QUICK MODE (1a3): the moment the mouse lifts, capture fires — keep drawing and each lift+    // supersedes the last capture with the union bbox.+    // 2.0.457 (John: "esc key should let me turn it back off"): the key listener STAYS after a quick capture;+    // the toolbar is still up, so Esc must still close it. The fade-out after a capture removes it.+    if (S.quick) { emit(null, true); }+    // v2.0.371 (John): in MULTI mode each finished shape auto-advances to the next color, so a+    // red circle then a blue circle needs zero clicks. White is excluded (invisible on light+    // pages); a manual swatch pick overrides and the cycle continues from there.+    else advanceColor();+  });+  function icon(paths, dash) {+    var n = document.createElementNS('http://www.w3.org/2000/svg', 'svg');+    n.setAttribute('viewBox', '0 0 24 24'); n.setAttribute('width', '14'); n.setAttribute('height', '14');+    n.setAttribute('fill', 'none'); n.setAttribute('stroke', 'currentColor');+    n.setAttribute('stroke-width', '1.9'); n.setAttribute('stroke-linecap', 'round'); n.setAttribute('stroke-linejoin', 'round');+    paths.forEach(function (dd, i) { var p2 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); p2.setAttribute('d', dd); if (dash && i === dash - 1) p2.setAttribute('stroke-dasharray', '2.5 2'); n.appendChild(p2); });+    return n;+  }+  var ICONS = {+    pen: [['M4 20l1.5-4.5L16 5a2.1 2.1 0 0 1 3 3L8.5 18.5 4 20z', 'M13.5 7.5l3 3']],+    arrow: [['M5 19L17 7', 'M9 7h8v8']],+    box: [['M4.5 5.5h15v13h-15z']],+    text: [['M6 6h12', 'M12 6v12']],+    snip: [['M4 8V5a1 1 0 0 1 1-1h3M16 4h3a1 1 0 0 1 1 1v3M20 16v3a1 1 0 0 1-1 1h-3M8 20H5a1 1 0 0 1-1-1v-3', 'M8.5 8.5h7v7h-7z'], 2],+    auto: [['M13 3L5 13h5l-1 8 8-10h-5l1-8']],+    multi: [['M4 4.5h10v10H4z', 'M10 9.5h10v10H10z']],+    undo: [['M8 5L4 9l4 4', 'M4 9h9a6 6 0 0 1 0 12h-2']],+    clear: [['M5 7h14', 'M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2', 'M7 7l1 13h8l1-13']],+    done: [['M4.5 12.5l5 5 10-11']],+    cancel: [['M6 6l12 12', 'M18 6L6 18']],+  };+  function btn(label, title, fn, id, iconKey) {+    var b = document.createElement('button');+    if (iconKey && ICONS[iconKey]) b.appendChild(icon(ICONS[iconKey][0], ICONS[iconKey][1]));+    b.appendChild(document.createTextNode(label)); b.title = title;+    b.style.cssText = 'background:none;border:1px solid transparent;border-radius:8px;color:#c9d1d9;padding:3px 8px;cursor:pointer;font:inherit;display:flex;align-items:center;gap:5px;';+    b.onmouseenter = function () { b.style.background = '#1c2128'; };+    b.onmouseleave = function () { b.style.background = S.tool === b.dataset.tool ? 'rgba(0,184,177,.13)' : 'none'; };+    b.onclick = fn; if (id) b.dataset.tool = id; bar.appendChild(b);+    if (title) { tipFor(b, title); }+    return b;+  }+  function selTool(id) { S.tool = id; setCursor(); [].forEach.call(bar.querySelectorAll('button[data-tool]'), function (x) { x.style.background = x.dataset.tool === id ? 'rgba(0,184,177,.13)' : 'none'; x.style.borderColor = x.dataset.tool === id ? 'rgba(0,184,177,.45)' : 'transparent'; }); }+  btn('Pen', 'Draw freehand. With Auto on, the capture fires the moment you release the mouse: cropped tight to your drawing, copied to your clipboard.', function () { selTool('pen'); }, 'pen', 'pen');+  btn('Arrow', 'Point at the thing you mean. Drag from tail to tip; the arrowhead lands where you release.', function () { selTool('arrow'); }, 'arrow', 'arrow');+  btn('Box', 'Frame a region with a rectangle. Drag corner to corner.', function () { selTool('box'); }, 'box', 'box');+  btn('Text', 'Drop a short text note onto the page. Click where the note should start.', function () { selTool('text'); }, 'text', 'text');+  btn('Snip', 'Capture exactly a region, like Win+Shift+S: drag a marquee and it is copied to your clipboard the moment you release.', function () { selTool('snip'); }, 'snip', 'snip');+  // v2.0.370 (John): the color control is now a one-click TRIO — red (default), blue, yellow —+  // with the rarer colors one click deeper in a flyout. The old single cycling dot made+  // red→blue three clicks; picking between about 3 colors must be instant.+  var SW_MAIN = ['#f85149', '#64abff', '#d29922'];+  var SW_MORE = ['#3fb950', '#8c6bf7', '#e6edf3'];+  var flyout = document.createElement('div');+  flyout.style.cssText = 'position:absolute;top:calc(100% + 8px);left:50%;transform:translateX(-50%);display:none;gap:7px;background:#161b22f2;border:1px solid #30363d;border-radius:999px;padding:7px 10px;box-shadow:0 6px 24px #000a;pointer-events:auto;';+  var more = document.createElement('button');+  function setColor(c) {+    S.color = c; setCursor(); flyout.style.display = 'none';+    [].forEach.call(wrap.querySelectorAll('button[data-sw]'), function (x) { x.style.borderColor = x.dataset.sw === c ? '#fff' : 'rgba(255,255,255,.25)'; });+    more.style.outline = SW_MORE.indexOf(c) >= 0 ? '2px solid #fff' : 'none';+  }+  // v2.0.371: the multi-mode auto-advance cycle — the quick trio then green/purple, never white.+  function advanceColor() {+    var CYCLE = ['#f85149', '#64abff', '#d29922', '#3fb950', '#8c6bf7'];+    setColor(CYCLE[(CYCLE.indexOf(S.color) + 1) % CYCLE.length]);+  }+  function swBtn(c, parent) {+    var s = document.createElement('button');+    s.dataset.sw = c;+    s.style.cssText = 'width:18px;height:18px;border-radius:50%;cursor:pointer;background:' + c + ';border:2px solid ' + (S.color === c ? '#fff' : 'rgba(255,255,255,.25)') + ';padding:0;flex:none;';+    s.onclick = function () { setColor(c); };+    parent.appendChild(s);+    return s;+  }+  SW_MAIN.forEach(function (c) { swBtn(c, bar); });+  more.style.cssText = 'width:18px;height:18px;border-radius:50%;cursor:pointer;border:2px solid rgba(255,255,255,.25);padding:0;flex:none;background:conic-gradient(#f85149,#d29922,#3fb950,#64abff,#8c6bf7,#f85149);';+  more.onclick = function () { flyout.style.display = flyout.style.display === 'none' ? 'flex' : 'none'; };+  bar.appendChild(more); tipFor(more, 'More colors: green, purple, white.');+  SW_MORE.forEach(function (c) { swBtn(c, flyout); });+  bar.appendChild(flyout);+  // v2.0.370 (John): Auto vs Multi as an explicit two-state control, not a lone toggle.+  var modeWrap = document.createElement('div');+  modeWrap.style.cssText = 'display:flex;border:1px solid #30363d;border-radius:8px;overflow:hidden;margin-left:2px;';+  function modeBtn(label, iconKey, val, tip) {+    var b = document.createElement('button');+    if (ICONS[iconKey]) b.appendChild(icon(ICONS[iconKey][0], ICONS[iconKey][1]));+    b.appendChild(document.createTextNode(label));+    b.style.cssText = 'background:none;border:none;color:#c9d1d9;padding:3px 8px;cursor:pointer;font:inherit;display:flex;align-items:center;gap:5px;';+    b.onclick = function () { S.quick = val; paintMode(); };+    modeWrap.appendChild(b); tipFor(b, tip);+    return b;+  }+  var mAuto = modeBtn('Auto', 'auto', true, 'One-shot (the default): draw one thing, lift the mouse, and it is captured and copied to your clipboard instantly.');+  var mMulti = modeBtn('Multi', 'multi', false, 'Compose: draw several shapes and NOTHING is captured until you click Done. The color auto-advances after each shape (red, blue, yellow, green, purple) so every shape is distinct — click a swatch any time to override.');+  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';+  }+  paintMode(); bar.appendChild(modeWrap);+  btn('Undo', 'Remove the last thing you drew. Click again to keep unwinding.', function () { S.strokes.pop(); redraw(); }, null, 'undo');+  btn('Clear', 'Erase every drawing on this page. The page itself is untouched.', function () { S.strokes = []; redraw(); }, null, 'clear');+  var done = btn('Done', 'Capture the annotated area (tight-cropped around your drawing), copy it to your clipboard, and leave the drawing visible so the AI thread that owns this window sees it too.', function () { emit(null, true); }, null, 'done');+  done.style.cssText += 'background:rgba(0,184,177,.13);border:1px solid rgba(0,184,177,.55);color:#00d4cb;font-weight:600;';+  btn('', 'Close the annotation toolbar and remove all drawings from the page (Esc does this too).', function () { exitAll(); }, null, 'cancel');+  function exitAll() { disarmKeys(); wrap.remove(); window.__pupAnnotate = null; window.__pupAnnotateResult = { done: false, ts: Date.now() }; }+  function onKey(e) { if (e.key === 'Escape' || e.keyCode === 27) { e.preventDefault(); e.stopPropagation(); exitAll(); } }+  // Esc reaches the overlay wherever focus sits: window and document in the capture phase, the canvas itself,+  // and the canvas takes focus when armed (a focused iframe would otherwise keep the key to itself).+  function armKeys() { addEventListener('keydown', onKey, true); document.addEventListener('keydown', onKey, true); cv.addEventListener('keydown', onKey, true); try { cv.tabIndex = -1; cv.style.outline = 'none'; cv.focus({ preventScroll: true }); } catch (e) {} }+  function disarmKeys() { removeEventListener('keydown', onKey, true); document.removeEventListener('keydown', onKey, true); cv.removeEventListener('keydown', onKey, true); }+  cv.addEventListener('pointerdown', function () { try { cv.focus({ preventScroll: true }); } catch (e) {} }, true);+  armKeys();+  // v2.0.72: called by the bridge the moment the screenshot is TAKEN (never before, so the+  // flash is not in the shot): camera-flash feedback over the captured region, instant toast+  // with the preview, then the drawings fade out so the user is back to normal surfing.+  window.__pupCaptureDone = function (region, previewUrl, line1) {+    try {+      var fl = document.createElement('div');+      if (!region || !region.w) throw 0; // skip the flash only (caught below): John's rule, flash the captured region or nothing — toast and fade still run+      var rg = region;+      fl.style.cssText = 'position:fixed;left:' + rg.x + 'px;top:' + rg.y + 'px;width:' + rg.w + 'px;height:' + rg.h + 'px;z-index:2147483647;background:none;border:2px dashed #00d4cb;border-radius:6px;opacity:1;pointer-events:none;transition:opacity .45s ease;box-sizing:border-box;';+      document.documentElement.appendChild(fl);+      setTimeout(function () { fl.style.opacity = '0'; }, 550);+      setTimeout(function () { try { fl.remove(); } catch (e) {} }, 1100);+    } catch (e) {}+    try {+      var old = document.getElementById('__pup_ann_toast'); if (old) old.remove();+      var t = document.createElement('div'); t.id = '__pup_ann_toast';+      t.style.cssText = 'position:fixed;left:50%;transform:translateX(-50%);bottom:18px;z-index:2147483647;background:#21262df2;border:1px solid #30363d;border-radius:12px;padding:10px;box-shadow:0 8px 30px #000c;font:12px Satoshi,system-ui,sans-serif;color:#c9d1d9;max-width:340px;cursor:pointer;';+      var cap = document.createElement('div'); cap.textContent = line1 || 'Captured'; cap.style.cssText = 'margin-bottom:7px;color:#00d4cb;font-weight:500;';+      t.appendChild(cap);+      // v2.0.135: the preview arrives as RAW RGBA pixels and is PAINTED (putImageData), never+      // LOADED — an <img src="data:..."> is a resource load the page's CSP img-src can veto+      // (Hacker News does), which showed the user a broken-image icon. Pixel writes to a canvas+      // are not loads; no site's CSP can break this. (String form kept for mixed-version overlap.)+      if (previewUrl && previewUrl.b64 && previewUrl.w) {+        try {+          var pc = document.createElement('canvas'); pc.width = previewUrl.w; pc.height = previewUrl.h;+          pc.style.cssText = 'display:block;max-width:320px;max-height:200px;border-radius:8px;border:1px solid #30363d;';+          var bin = atob(previewUrl.b64), n = bin.length, arr = new Uint8ClampedArray(n);+          for (var i2 = 0; i2 < n; i2++) arr[i2] = bin.charCodeAt(i2);+          pc.getContext('2d').putImageData(new ImageData(arr, previewUrl.w, previewUrl.h), 0, 0);+          t.appendChild(pc);+        } catch (e2) {}+      } else if (previewUrl && typeof previewUrl === 'string') { var im = document.createElement('img'); im.src = previewUrl; im.style.cssText = 'display:block;max-width:320px;max-height:200px;border-radius:8px;border:1px solid #30363d;'; t.appendChild(im); }+      t.onclick = function () { t.remove(); };+      document.documentElement.appendChild(t);+      setTimeout(function () { try { t.remove(); } catch (e) {} }, 4500);+    } catch (e) {}+    // fade the drawings out (John: after the snapshot, back to normal surfing)+    setTimeout(function () {+      try {+        cv.style.transition = 'opacity .8s ease'; cv.style.opacity = '0';+        setTimeout(function () { try { wrap.remove(); window.__pupAnnotate = null; window.__pupCaptureDone = null; disarmKeys(); } catch (e) {} }, 900);+      } catch (e) {}+    }, 2200);+  };+  window.__pupAnnotate = {+    show: function () { window.__pupAnnotateHidden = false; wrap.style.display = ''; bar.style.display = 'flex'; cv.style.pointerEvents = 'auto'; armKeys(); },+    setMode: function (m) { S.quick = m !== 'multi'; paintMode(); },+  };+  addEventListener('resize', fit);+  document.documentElement.appendChild(wrap);+  fit(); selTool(initTool);+  return 'started';+};\ No newline at end of file
src/extension/worker.js+29
@@ -239,6 +239,35 @@ async function pupExtPoll() {         } catch (e) {}       }       out = { id: cmd.id, ok: true, hrefs, favicon, url: tab.url };+    } else if (cmd.action === 'inject') {+      // Inject an extension FILE into the page's MAIN world. File injection is not subject to the page's+      // CSP (the eval action is), so the annotate overlay reaches Google Docs and the wiki (2.0.569).+      const tab = await pupExtTab(cmd);+      await chrome.scripting.executeScript({ target: { tabId: tab.id }, world: cmd.world || 'MAIN', files: [cmd.file] });+      out = { id: cmd.id, ok: true, tabId: tab.id };+    } else if (cmd.action === 'call') {+      // window[fn](...args) in the MAIN world by injected FUNCTION (CSP-exempt): { value } | { missing } | { error }.+      const tab = await pupExtTab(cmd);+      const res = await chrome.scripting.executeScript({ target: { tabId: tab.id }, world: 'MAIN', func: (name, args) => {+        try { const f = window[name]; if (typeof f !== 'function') return { missing: true }; const v = f.apply(window, args || []); return { value: v === undefined ? null : v }; }+        catch (e) { return { error: String(e && e.message || e) }; } }, args: [cmd.fn || '', cmd.args || []] });+      out = Object.assign({ id: cmd.id, ok: true, tabId: tab.id }, (res && res[0] && res[0].result) || {});+    } else if (cmd.action === 'read') {+      // window[name] (cleared when asked), plus the device pixel ratio.+      const tab = await pupExtTab(cmd);+      const res = await chrome.scripting.executeScript({ target: { tabId: tab.id }, world: 'MAIN', func: (name, clear) => {+        const v = window[name]; if (clear) window[name] = null;+        return { value: v === undefined ? null : v, dpr: window.devicePixelRatio || 1 }; }, args: [cmd.name || '', !!cmd.clear] });+      out = Object.assign({ id: cmd.id, ok: true, tabId: tab.id }, (res && res[0] && res[0].result) || {});+    } else if (cmd.action === 'captureVisible') {+      const tab = await pupExtTab(cmd);+      const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { format: 'png' });+      out = { id: cmd.id, ok: true, tabId: tab.id, dataUrl };+    } else if (cmd.action === 'reload') {+      // Answer first, then restart this worker so a freshly installed pup's extension files take effect+      // without the browser relaunching (2.0.569: an update used to wait until every thread's window closed).+      out = { id: cmd.id, ok: true, reloading: true };+      setTimeout(() => { try { chrome.runtime.reload(); } catch (e) {} }, 800);     } else if (cmd.action === 'newtab') {       // a TAB in the SAME window: the channel could only create new WINDOWS, so it could not show many       // pages in one window the way the CDP lane does.