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.
master
John Lauer
v2.0.705 (insiders): Annotate's quick colors are red, blue and green: the muddy yellow/orange third color is gone from…
bdd3a30
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
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.
// 2.0.701 (John, 2026-10-06): MULTI is the default; Single (the old Auto) is one keypress away.
var S = { tool: 'pen', color: '#f85149', strokes: [], cur: null, quick: initMode === 'auto', 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';
// No innerHTML: pages that enforce Trusted Types (Google Docs) refuse it. An <img> src is not a sink.
while (spr.firstChild) spr.removeChild(spr.firstChild);
const sprImg = document.createElement('img'); sprImg.width = 24; sprImg.height = 24; sprImg.draggable = false; sprImg.alt = '';
sprImg.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(S.tool === 'pen' ? penSvg() : arrowSvg());
spr.appendChild(sprImg);
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.
// 2.0.705 (John, 2026-10-07): the third quick color is GREEN, not the amber yellow. The point of the colors is
// to tell the AI about them ("the green circle is the regulator"), and red, blue and green are names nobody has
// to think about; "the yellow/orange one" was ambiguous. Yellow moves to the flyout and out of the auto cycle.
var SW_MAIN = ['#f85149', '#64abff', '#3fb950'];
var SW_MORE = ['#8c6bf7', '#e3b341', '#e6edf3'];
var SW_NAME = { '#f85149': 'Red', '#64abff': 'Blue', '#3fb950': 'Green', '#8c6bf7': 'Purple', '#e3b341': 'Yellow', '#e6edf3': 'White' };
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. 2.0.705: red, blue, green, purple; never yellow or white.
function advanceColor() {
var CYCLE = ['#f85149', '#64abff', '#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);
if (SW_NAME[c]) tipFor(s, SW_NAME[c]); // 2.0.705: name the color, so it is clear what to call it when talking to the AI
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,#e3b341,#3fb950,#64abff,#8c6bf7,#f85149);';
more.onclick = function () { flyout.style.display = flyout.style.display === 'none' ? 'flex' : 'none'; };
bar.appendChild(more); tipFor(more, 'More colors: purple, yellow, 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('Single', 'auto', true, 'Single (S): draw one thing, lift the mouse, and it is captured and copied to your clipboard instantly. Space switches back to Multi.');
var mMulti = modeBtn('Multi', 'multi', false, 'Multi (the default, M): draw several shapes and NOTHING is captured until you press Enter or click Done. The color auto-advances after each shape (red, blue, green, purple) so every shape is distinct — click a swatch any time to override.');
// 2.0.701: a one-line hint under the toolbar that says how to finish in the current mode and how to switch.
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);
// 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';
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');
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() }; }
// 2.0.701 (John): Enter finishes, Space toggles Multi/Single, M and S pick one. Esc still cancels. Keys with a
// modifier held are left to the page and the browser.
function onKey(e) {
if (e.key === 'Escape' || e.keyCode === 27) { e.preventDefault(); e.stopPropagation(); exitAll(); return; }
if (e.ctrlKey || e.metaKey || e.altKey) return;
var k = (e.key || '').toLowerCase(), hit = true;
if (k === 'enter') { if (S.strokes.length) emit(null, true); }
else if (k === ' ' || k === 'spacebar') { S.quick = !S.quick; paintMode(); }
else if (k === 'm') { S.quick = false; paintMode(); }
else if (k === 's') { S.quick = true; paintMode(); }
else hit = false;
if (hit) { e.preventDefault(); e.stopPropagation(); }
}
// 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';
};