app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
← 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.
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';+};
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.