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
Annotation and snipping land in pup (John: claude desktop can annotate in its browser, add it to pup, and build in a screenshot snippet like shift+win+s). New pup_annotate verb plus an Annotate this page task on every window's jump list: an in-page overlay with pen, arrow, box, text, color cycling, undo, and a Snip tool whose marquee captures just that region immediately, exactly the win+shift+s flow. Done captures a PNG and deliberately leaves the drawing in the DOM, because pup windows are thread-owned: the annotation is a message, and the owning AI thread's next screenshot shows exactly what the human circled. Captures store under screenshots/ and surface as session lastAnnotation; a caption confirms the handoff. jlAnnotate sub-setting, default on
4 files changed
+154−9
adom-bridge-puppeteer-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "2.0.48",- "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.48/adom-bridge-puppeteer-v2.0.48.zip",- "sha256": "02f08c6ef2a42774473c32e6da887899a919a38de377a1947579143df1e46cbb",- "size": 650601,+ "version": "2.0.49",+ "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.49/adom-bridge-puppeteer-v2.0.49.zip",+ "sha256": "c79a022610c6174260024d1de1a89747b1d38c7542829b985e3c98847a06b7a2",+ "size": 654831, "verbPrefixes": [ "browser_", "desktop_recorder_",
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-2.0.48+2.0.49
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "2.0.48",+ "version": "2.0.49", "description": "Drives a browser ALREADY on the machine \u2014 installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) \u2014 launched with a FRESH isolated profile, so the common case has NO ~150 MB Chrome-for-Testing download and it never touches the user's real tabs/logins. Binds its local server to LOOPBACK (127.0.0.1) only, so no Windows Firewall 'allow Node through the firewall' prompt ever pops. Chrome for Testing is fetched only as a LAST RESORT if the machine has no Chromium browser at all. SPAWN-VERIFIED fallback: pup tries each detected browser (Chrome \u2192 Edge \u2192 CfT) and actually launches it, falling through to the next if one won't start (and handling browsers like Edge that relaunch onto another PID), so a corrupt/partial install never hard-fails an open. The browser that launches is cached as the default (persisted across restarts) so it isn't re-probed each time. On spawn the bridge SELF-WARMS: detects browsers + disk, and only if the box has no Chrome/Edge and no cached CfT does it background-fetch CfT so the first open is instant. pup_use pins a specific browser (chrome | edge | cft | auto); {browser:'chrome',install:true} downloads & installs REAL Google Chrome (user-scoped, no admin) on an Edge-only box. pup_readiness (READ-ONLY) reports which browser will be used + the full candidate list + the default + free disk. Every failure returns a specific errorCode + _hint (chrome_install_no_disk, chrome_download_network, chrome_install_permission, chrome_stable_not_detected, \u2026). Launches with FULL browser permissions by default for full debug (strictPermissions:true opts out). Headful automation: open/close windows+tabs, navigate, screenshot, eval JS, record. pup is a GENERIC, non-profile browser only \u2014 every session is a fresh isolated Chrome/Edge process, never the user's real signed-in browser; profile defaults to sessionId. To drive the user's REAL Chrome/Edge (their profile, cookies, SSO), use the Adom browser EXTENSION's nbrowser_* verbs \u2014 a separate bridge, not pup's job. macOS (v1.9.74+): full parity with mac-native mechanics \u2014 background-by-default is the app-level hide (windows stay drivable/screenshottable while hidden), raise/lower map to unhide+activate/hide, pup_alert_window posts a native toast whose click focuses the window, pup_use {browser:'chrome',install:true} installs Chrome from the official dmg, and taskbar identity/badging remains Windows-only.", "homepage": "https://wiki.adom.inc/adom/adom-bridge", "author": "Adom Inc.",
src/server.js+148−3@@ -3049,6 +3049,7 @@ function pupSettings() { D.jlViewTasks = 'on'; D.jlCloseAll = 'on'; // v2.0.42 (John: "can you add a jump list to open the pup dashboard?") D.jlDashboard = 'on';+ D.jlAnnotate = 'on'; // v2.0.49 // v1.9.293 (John): the 'NOT foregrounded by Adom' caption is now OFF by default. He is right that // input-timing cannot reliably tell his own raises (click / alt+tab / window click) from a rare // JS raise - and Windows blocks most background foreground-steals anyway, so the case it exists@@ -3858,7 +3859,7 @@ async function updateWikiJumplist(session, sessionId, opts) { // are the same for a wiki window regardless of view (both switch-view tasks always show), so freezing // costs at most a slightly stale menu, and buys zero churn — matching the appId freeze. if (session._jlFrozen) return;- const jlKey = `${appId}|${view || 'none'}|${pupSettings().jlViewTasks}|${pupSettings().jlCloseAll}|${pupSettings().jlDashboard}`;+ const jlKey = `${appId}|${view || 'none'}|${pupSettings().jlViewTasks}|${pupSettings().jlCloseAll}|${pupSettings().jlDashboard}|${pupSettings().jlAnnotate}`; if (session._wikiJumplistKey === jlKey) return; // v2.0.3: coalesce the jump-list settle churn. On open, the bare per-session appId and the // public->authed view each produce a distinct (appId|view) commit, so a single open committed the@@ -3942,6 +3943,13 @@ async function updateWikiJumplist(session, sessionId, opts) { // v2.0.42 (John: "can you add a jump list to open the pup dashboard?"): one click on any pup // taskbar icon opens the live dashboard — the map of every pup window/tab and its owning AI // thread. The verb reuses the one dashboard window on repeat clicks, so this is idempotent.+ // v2.0.49 (John: "claude desktop can annotate — add it to pup"): draw on this page from the+ // taskbar; Done captures it for the AI thread that owns the window.+ if (pupSettings().jlAnnotate !== 'off') tasks.push(pupTask('pup_annotate', { sessionId }, {+ title: 'Annotate this page',+ description: 'Draw on this page (pen, arrow, box, text) — Done captures it so the AI thread owning this window sees exactly what you marked',+ iconPath: _taskIcon,+ })); if (pupSettings().jlDashboard === 'on') tasks.push(pupTask('pup_dashboard', {}, { title: 'Open pup dashboard', description: 'Live dashboard of every pup window: owning AI thread, tabs with favicons, foreground state and why, uptime (reuses the dashboard window if already open)',@@ -5247,6 +5255,118 @@ function appIconReprimand(url, hadFavicon) { // relaunches the browser under the other cookie jar and takes several seconds, so WITHOUT this a // click felt like nothing happened at all. Same `id` on every call so the start caption is // REPLACED by the result caption instead of stacking. Best-effort; never throws.++// ─── v2.0.49 ANNOTATION (John: "claude desktop has annotate in its browser — add it to pup"):+// an in-page canvas overlay the HUMAN draws on (pen/arrow/box/text), started from the window's+// jump list or the pup_annotate verb. pup's twist over Claude Desktop: the window is THREAD-OWNED,+// so a Done'd annotation is a MESSAGE — the drawing stays in-DOM, so the owning AI thread's next+// screenshot shows exactly what the human circled. Injected via CDP (immune to page CSP), zero+// external resources.+const ANNOTATE_OVERLAY_JS = `(() => {+ if (window.__pupAnnotate) { window.__pupAnnotate.show(); return 'shown'; }+ var S = { tool: 'pen', color: '#ff4d5e', strokes: [], cur: null };+ var COLORS = ['#ff4d5e', '#ffd23f', '#39d98a', '#38bdf8', '#c084fc', '#ffffff'];+ 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;cursor:crosshair;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:#111827ee;border:1px solid #2dd4bf55;border-radius:999px;padding:7px 12px;pointer-events:auto;font:13px system-ui,sans-serif;color:#e5e7eb;box-shadow:0 6px 24px #000a;';+ wrap.appendChild(bar);+ var ctx = cv.getContext('2d');+ 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 = '#5eead4'; 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); }+ 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(); } 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) { bar.style.display = 'none'; cv.style.pointerEvents = 'none'; window.__pupAnnotateResult = { done: true, snip: r, shapes: S.strokes.length, ts: Date.now() }; }+ return;+ }+ S.strokes.push(S.cur); S.cur = null; redraw();+ });+ function btn(label, title, fn, id) {+ var b = document.createElement('button');+ b.textContent = label; b.title = title;+ b.style.cssText = 'background:none;border:1px solid transparent;border-radius:8px;color:#e5e7eb;padding:3px 8px;cursor:pointer;font:inherit;';+ b.onmouseenter = function () { b.style.background = '#ffffff14'; };+ b.onmouseleave = function () { b.style.background = S.tool === id ? '#2dd4bf22' : 'none'; };+ b.onclick = fn; if (id) b.dataset.tool = id; bar.appendChild(b); return b;+ }+ function selTool(id) { S.tool = id; [].forEach.call(bar.querySelectorAll('button[data-tool]'), function (x) { x.style.background = x.dataset.tool === id ? '#2dd4bf22' : 'none'; x.style.borderColor = x.dataset.tool === id ? '#2dd4bf66' : 'transparent'; }); }+ btn('✎ Pen', 'freehand', function () { selTool('pen'); }, 'pen');+ btn('↗ Arrow', 'arrow', function () { selTool('arrow'); }, 'arrow');+ btn('▭ Box', 'rectangle', function () { selTool('box'); }, 'box');+ btn('T Text', 'text note', function () { selTool('text'); }, 'text');+ btn('\u25a3 Snip', 'marquee a region — captures it immediately (like Win+Shift+S)', function () { selTool('snip'); }, 'snip');+ var dot = document.createElement('button');+ dot.title = 'color'; dot.style.cssText = 'width:20px;height:20px;border-radius:50%;border:2px solid #fff8;cursor:pointer;background:' + S.color + ';';+ dot.onclick = function () { var i = (COLORS.indexOf(S.color) + 1) % COLORS.length; S.color = COLORS[i]; dot.style.background = S.color; };+ bar.appendChild(dot);+ btn('↶ Undo', 'undo last', function () { S.strokes.pop(); redraw(); });+ btn('✕ Clear', 'clear all', function () { S.strokes = []; redraw(); });+ var done = btn('✓ Done', 'capture for the owning AI thread', function () {+ bar.style.display = 'none'; cv.style.pointerEvents = 'none';+ window.__pupAnnotateResult = { done: true, shapes: S.strokes.length, ts: Date.now() };+ });+ done.style.cssText += 'background:#2dd4bf22;border:1px solid #2dd4bf88;color:#5eead4;font-weight:600;';+ btn('✖', 'cancel (remove drawing)', function () { wrap.remove(); window.__pupAnnotate = null; window.__pupAnnotateResult = { done: false, ts: Date.now() }; });+ window.__pupAnnotate = { show: function () { wrap.style.display = ''; bar.style.display = 'flex'; cv.style.pointerEvents = 'auto'; } };+ addEventListener('resize', fit);+ document.documentElement.appendChild(wrap);+ fit(); selTool('pen');+ return 'started';+})()`;++async function startAnnotation(session, sessionId, callerThread) {+ const tab = (session.activeTabId && (session.tabs || []).find(t => t.tabId === session.activeTabId)) || (session.tabs || [])[0];+ const pg = tab && tab.page;+ if (!pg) throw new Error('no page to annotate');+ await pg.evaluate((code) => eval(code), ANNOTATE_OVERLAY_JS);+ console.log(`[annotate] "${sessionId}" overlay started (by ${callerThread || 'unknown'})`);+ // Fire-and-forget: wait up to 10 min for Done/cancel, then capture. The drawing STAYS in the DOM+ // after Done (pointer-events off), so the owning thread's next screenshot shows it.+ (async () => {+ try {+ await pg.waitForFunction('window.__pupAnnotateResult', { timeout: 600000, polling: 1000 });+ const res = await pg.evaluate(() => { const r = window.__pupAnnotateResult; window.__pupAnnotateResult = null; return r; });+ if (!res || !res.done) { console.log(`[annotate] "${sessionId}" cancelled`); return; }+ const ts = new Date().toISOString().replace(/[:.]/g, '-');+ const isSnip = res.snip && res.snip.w > 8 && res.snip.h > 8;+ const fp = path.join(SHOTS_DIR, `annotate-${isSnip ? 'snip-' : ''}${sessionId}-${ts}.png`);+ // v2.0.49 SNIP (John: "i hit shift+windows+s all the time — build a screenshot snippet in"):+ // the marquee captures JUST that region (clip is viewport CSS px, same space as clientX/Y),+ // drawings inside the region included since they're in-DOM.+ await pg.screenshot(isSnip ? { path: fp, clip: { x: res.snip.x, y: res.snip.y, width: res.snip.w, height: res.snip.h } } : { path: fp });+ session._lastAnnotation = { path: fp, ts: Date.now(), shapes: res.shapes, snip: isSnip ? res.snip : null };+ console.log(`[annotate] "${sessionId}" CAPTURED ${isSnip ? `SNIP ${res.snip.w}x${res.snip.h}` : `${res.shapes} shape(s)`} -> ${fp} — drawing left in-DOM so the owning thread's next screenshot shows it`);+ wikiCaption(isSnip ? '\u25a3 Snippet captured — the AI thread owning this window can now see it' : `✎ Annotation captured (${res.shapes} shape${res.shapes === 1 ? '' : 's'}) — the AI thread owning this window can now see it`, 6000).catch(() => {});+ } catch (e) { console.log(`[annotate] "${sessionId}" wait/capture ended: ${e.message}`); }+ })();+}+ async function wikiCaption(text, expiresInMs) { try { // v1.9.11: AD ≥1.9.144 exposes desktop_caption on the LOOPBACK direct API (the path bridges@@ -9688,6 +9808,7 @@ const SETTINGS_DEF=[ {k:'jlViewTasks',l:'',d:'Wiki view tasks: Switch to logged-in view / Switch to public view on every wiki window.',dep:'jumpLists',subtgl:1,img:'jlviewtasks'}, {k:'jlCloseAll',l:'',d:'The Close all pup windows task on every pup icon.',dep:'jumpLists',subtgl:1,img:'jlcloseall'}, {k:'jlDashboard',l:'',d:'The Open pup dashboard task on every pup icon: one click opens the live window/tab/thread map.',dep:'jumpLists',subtgl:1},+ {k:'jlAnnotate',l:'',d:'The Annotate this page task: draw on the page; Done captures it for the AI thread that owns the window.',dep:'jumpLists',subtgl:1}, {k:'fgWatch',l:'20s foreground watch',d:'For 20s after a window opens or navigates in the background, watch whether something (JavaScript, Chrome, a focus change) brings it to the foreground, and caption that it was not Adom. Never moves the window.'}, {k:'fgCaption',l:'Caption on AI foreground',d:'When an AI brings a window to your screen, show its stated reason as an on-screen caption. The AI is always forced to give a reason; this shows it to you.'}, {k:'fgCaptionMs',l:'',d:'',slider:[500,5000,250],dep:'fgCaption',test:1},@@ -10013,7 +10134,7 @@ const server = http.createServer(async (req, res) => { reportIssues: ['on', 'off'], autoLogin: ['on', 'off'], minimalTouch: ['on', 'off'], taskbarGrouping: ['split', 'grouped'], aumidIconStyle: ['standard', 'category'],- jlViewTasks: ['on', 'off'], jlCloseAll: ['on', 'off'], jlDashboard: ['on', 'off'],+ jlViewTasks: ['on', 'off'], jlCloseAll: ['on', 'off'], jlDashboard: ['on', 'off'], jlAnnotate: ['on', 'off'], notMineCaption: ['on', 'off'], ownerChip: ['on', 'off'], titleTag: ['on', 'off'], titlePrefix: ['on', 'off'], };@@ -10029,7 +10150,7 @@ const server = http.createServer(async (req, res) => { // v1.9.279: a taskbar-identity setting must take effect on EXISTING windows immediately, // not just future ones. Reconcile now re-stamps every live window to match (idempotent). if (['aumidIcons', 'taskbarGrouping', 'aumidIconStyle'].includes(key)) reconcileModel('setting ' + key).catch(() => {});- if (['jumpLists', 'jlViewTasks', 'jlCloseAll', 'jlDashboard'].includes(key)) refreshAllJumplists().catch(() => {}); // v1.9.320: OFF clears the menu, ON/sub re-commits+ if (['jumpLists', 'jlViewTasks', 'jlCloseAll', 'jlDashboard', 'jlAnnotate'].includes(key)) refreshAllJumplists().catch(() => {}); // v1.9.320: OFF clears the menu, ON/sub re-commits if (key === 'ownerChip') applyOwnerChipSetting(val === 'on').catch(() => {}); return done({ ok: true, settings: pupSettings() }); }@@ -10510,6 +10631,7 @@ const server = http.createServer(async (req, res) => { V('pup_press_key', 'Press a key, optionally with modifier chords held (Ctrl+A, Ctrl+V, Shift+Tab).', { sessionId: 'required str', key: 'required str', modifiers: 'optional array — any of ["Control","Shift","Alt","Meta"] (aliases ctrl/cmd/command/option accepted); held down around the key press' }, 15, { sessionId: 'work', key: 'a', modifiers: ['Control'] }), V('pup_NATIVE_INPUT_WARNING', '⛔ READ BEFORE USING AD\'s desktop_click / desktop_type / desktop_press_key ON A PUP WINDOW. Native OS input does NOT target a window — it goes to WHATEVER WINDOW HAS OS FOCUS at that instant. This user runs many AI threads and alt-tabs away from a foregrounded window almost immediately. If you fire native input without warning them, your keystrokes land in THEIR document (this happened: a username was typed into the user\'s own window). REQUIRED: notify_user toast + desktop_caption FIRST, tell them explicitly "don\'t type or click until I say done", wait until the window is genuinely foreground, re-verify immediately before EACH event, and caption when done. pup_assisted_click already does all of this — use it. And ALWAYS prefer CDP input (pup_click/pup_type/pup_hover): it needs no foreground and disturbs nobody. A native verb returning ok:true means the event was dispatched to the OS, NOT that it reached your window.', {}, 0, {}), V('pup_set_viewport', 'Pin the page viewport (CDP device-metrics) to an exact size for deterministic screenshots — OR clear that pin. ⚠ A pin PERSISTS: it survives navigation and hard reload, and while set the window stops tracking OS resizing (a human resizing it sees no reflow, which looks exactly like an app layout bug). ALWAYS clear it when done. Reported as emulatedViewport in pup_status / pup_list_windows.', { sessionId: 'required str', width: 'int — required unless reset', height: 'int — required unless reset', deviceScaleFactor: 'optional number (default 1)', reset: 'optional bool — CLEAR the override and track the OS window again (width/height not needed)' }, 15, { sessionId: 'work', reset: true }),+ V('pup_annotate', 'Start the HUMAN annotation overlay on a window\'s active tab (pen/arrow/box/text toolbar injected in-page; also on every window\'s taskbar jump list as "Annotate this page"). Done captures a PNG AND leaves the drawing in the DOM, so the owning thread\'s next pup_screenshot shows exactly what the user marked — check session.lastAnnotation via pup_list_windows. action:"cancel" removes the overlay.', { sessionId: 'required window', action: 'optional "cancel"' }, 15, {}), V('pup_dashboard', 'Open the LIVE pup management dashboard in its OWN window (its own profile, so its own taskbar icon). Shows every window pup is driving as taskbar button -> window(s) -> tabs with each tab favicon, owning AI thread, foreground/background + the reason given to foreground it, highFps lease, uptime, and WHY each overlay icon was chosen. Live over SSE. Reuses the one window on repeat calls; backgrounded by default like any pup window.', {}, 30, {}), V('pup_deps', 'Report pup dependency health (wiki issue #15): depsPresent, selfInstalledThisBoot, lastError. Use this to answer "is pup broken?" without reading spawn logs.', { }, 10, {}), V('pup_hover', 'Move the pointer over a target WITHOUT clicking (reveals tooltips / hover menus). Glides the visible Adom cursor there too.', { sessionId: 'required str', selector: 'optional str', x: 'optional int', y: 'optional int', intent: 'optional str — narrated on the cursor label', steps: 'optional int (default 8)' }, 15, { sessionId: 'work', selector: '#menu' }),@@ -13529,6 +13651,29 @@ const server = http.createServer(async (req, res) => { sendJSON(res, { success: true, output: JSON.stringify({ ok: true, url: args.url, sessionId: resolvedId, tabId, ...(authSteerHint(args.url) ? { _hint: authSteerHint(args.url).trim() } : {}) }) }); return; }+ case 'pup_annotate': {+ // start/cancel the human annotation overlay on a window's active tab. Jump-list clicks+ // arrive as user-taskbar-menu (ownership-gate exempt); threads annotate their OWN windows.+ const sessionId = args.sessionId;+ if (!sessionId || !sessions.has(sessionId)) { sendJSON(res, { success: false, error: `Session "${sessionId}" not found` }); return; }+ const session = sessions.get(sessionId);+ try {+ if (args.action === 'cancel') {+ const tab0 = (session.tabs || [])[0];+ if (tab0 && tab0.page) await tab0.page.evaluate(() => { const w = document.getElementById('__pup_annotate'); if (w) w.remove(); window.__pupAnnotate = null; });+ sendJSON(res, { success: true, output: JSON.stringify({ ok: true, cancelled: true }) });+ return;+ }+ await startAnnotation(session, sessionId, _callerThread);+ // the human is about to draw — the window must be in front of them. A jump-list click IS+ // the user asking, so this raise is user-sanctioned by construction.+ session._foreground = true; session._userActiveAt = Date.now();+ try { const t0 = (session.tabs || [])[0]; if (t0 && t0.page) await t0.page.bringToFront(); } catch (e) {}+ try { await osRaiseSessionWindow(session, sessionId, 'you clicked Annotate: draw on the page, then hit Done'); } catch (e) {}+ sendJSON(res, { success: true, output: JSON.stringify({ ok: true, started: true, _hint: 'Overlay is live: pen/arrow/box/text, color dot cycles, Done captures (drawing stays visible so the owning AI thread\'s next screenshot shows it), \u2716 cancels.' }) });+ } catch (e) { sendJSON(res, { success: false, error: `annotate failed: ${e.message}` }); }+ return;+ } case 'pup_dashboard': { // v1.9.231 (John): the dashboard ALWAYS gets its OWN pup window, never a tab in someone // else's, so it carries its own taskbar icon (its served icon.svg) and can be found and