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.558 (insiders): Extension-lane taskbar identity: late rebind covers unbound, dead-hwnd and unstamped ext windows; pup_audit_icons reads identity back from Windows; pup_rescan judges ext sessions by the lane.
2 files changed
+75
src/extension/tile.jsadded+68@@ -0,0 +1,68 @@+// Adom Pup thread tile: the ONE renderer for the taskbar name tile (the thread's name on the teal+// plate, every ICO entry drawn at its own size). Two hosts run this same file:+// - the extension service worker (action "tile"): OffscreenCanvas, no page involved, so a page's+// Content-Security-Policy cannot block it (claude.ai and the wiki did, 2026-09-14, and the tile+// silently fell back to the per-window icon);+// - a page evaluation on the CDP lane: the Rust side embeds this file verbatim (include_str!).+// Returns { lines, size, tiles: { <px>: base64 RGBA } }. Keep the layout identical in both hosts.+self.pupRenderTile = async function (nm, fontUrl, sizes, R) {+ const mk = (w, h) => (typeof document !== 'undefined')+ ? Object.assign(document.createElement('canvas'), { width: w, height: h })+ : new OffscreenCanvas(w, h);+ try { const f = new FontFace('FGtile', 'url(' + fontUrl + ')'); await f.load(); (self.fonts || document.fonts).add(f); } catch (e) {}+const box = 256;+ const cv = mk(box, box); const ctx = cv.getContext('2d');+ const setF = (s) => { ctx.font = '700 ' + s + "px FGtile,'Segoe UI',sans-serif"; };+ const wid = (t, s) => { setF(s); return ctx.measureText(t).width; };+ const FLOOR = box * 0.36;+ const wFor = (i, total) => (total === 2 && i === 0) ? box * 0.78 : box * 0.90; // text starts at 7.5% in; 0.90 keeps the right edge inside the plate (Node: 0.92 from 5.5%)+ const fitSize = (lines) => { let s = box * 0.9; for (let i = 0; i < 90; i++) { let ok = true; for (let j = 0; j < lines.length; j++) { if (wid(lines[j], s) > wFor(j, lines.length)) { ok = false; break; } } if (ok && lines.length * s * 0.95 <= box * 0.88) break; s *= 0.95; } return s; };+ let best = null, bf = 0;+ // whole words first (split on spaces, after hyphens and slashes), two lines, only if it fits at or above the floor+ const toks = nm.split(/\s+/).flatMap(t => t.split(/(?<=[-\/])/)).filter(Boolean);+ if (toks.length > 1) {+ for (let k = 1; k < toks.length; k++) {+ const glue = (t) => t.reduce((a, x) => a + ((a && !/[-\/]$/.test(a)) ? ' ' : '') + x, '');+ const c = [glue(toks.slice(0, k)), glue(toks.slice(k))]; const f = fitSize(c);+ if (f >= FLOOR && f > bf) { bf = f; best = c; }+ }+ const one = fitSize([nm]); if (one >= FLOOR && one > bf) { bf = one; best = [nm]; }+ }+ if (!best) {+ const cands = [[nm]]; const sp = nm.lastIndexOf(' '), hy = nm.indexOf('-');+ if (sp > 0) cands.push([nm.slice(0, sp), nm.slice(sp + 1)]); else if (hy > 0) cands.push([nm.slice(0, hy + 1), nm.slice(hy + 1)]);+ if (sp < 0 && hy < 0 && nm.length >= 7) { const m = Math.ceil(nm.length / 2); cands.push([nm.slice(0, m), nm.slice(m)]); }+ best = [nm]; bf = 0;+ for (const c of cands) { const f = fitSize(c); if (f > bf) { bf = f; best = c; } }+ if (bf < FLOOR) {+ bf = FLOOR;+ const take = (str, maxw) => { let n = 0; for (let i = 1; i <= str.length; i++) { if (wid(str.slice(0, i), FLOOR) <= maxw) n = i; else break; } return n; };+ let rest = String(nm).trim(); const out = [];+ for (let ln = 0; ln < 2 && rest; ln++) {+ let n = take(rest, wFor(ln, 2));+ if (n >= rest.length) { out.push(rest); rest = ''; break; }+ if (n < 1) n = 1;+ const seg = rest.slice(0, n + 1);+ const bp = Math.max(seg.lastIndexOf(' '), seg.lastIndexOf('-'));+ const cut = (bp >= Math.ceil(n * 0.5)) ? bp + (seg.charAt(bp) === '-' ? 1 : 0) : n;+ out.push(rest.slice(0, cut).trim());+ rest = rest.slice(cut).replace(/^[ -]+/, '');+ }+ best = out.filter(Boolean);+ }+ }+ // t10: draw EVERY entry at its own size (plate rx = s/5.5, name bottom-left in the on-accent colour), so the+ // glyphs are rasterised by Skia for that exact pixel grid; the 256 layout only decides lines and font size.+ const tiles = {};+ for (const s of sizes) {+ const big = mk(s, s); const c = big.getContext('2d');+ const k = s / box, r = s / 5.5;+ c.beginPath(); c.moveTo(r, 0); c.arcTo(s, 0, s, s, r); c.arcTo(s, s, 0, s, r); c.arcTo(0, s, 0, 0, r); c.arcTo(0, 0, s, 0, r); c.closePath(); c.fillStyle = '#00b8b1'; c.fill();+ c.font = '700 ' + (bf * k) + "px FGtile,'Segoe UI',sans-serif"; c.fillStyle = '#05221f'; c.textAlign = 'left'; c.textBaseline = 'alphabetic';+ const lh = bf * k * 0.95, mx = s * 0.075, baseY = s - s * 0.075 - bf * k * 0.14;+ best.forEach((ln, i) => c.fillText(ln, mx, baseY - (best.length - 1 - i) * lh));+ const d = c.getImageData(0, 0, s, s).data; let bin = ''; const CH = 8192; for (let i = 0; i < d.length; i += CH) bin += String.fromCharCode.apply(null, d.subarray(i, Math.min(i + CH, d.length)));+ tiles[s] = btoa(bin);+ }+ return { lines: best, size: Math.round(bf), tiles };+};
src/extension/worker.js+7@@ -4,6 +4,9 @@ // keeps the per-tab hover title ("Adom Pup — <thread>'s window") and actively CLEARS any badge // an older worker painted. const CANDIDATE_PORTS = [64230, 8851];+// The thread-tile renderer (ONE implementation, shared with the CDP lane's page evaluation).+try { importScripts('tile.js'); } catch (e) {}+ // pup STAGES port.json into this extension folder with the bridge's REAL port. Read it: a FRESH profile has // empty chrome.storage, so without this the worker can only find pup when it happens to be on one of the two@@ -210,6 +213,10 @@ async function pupExtPoll() { args: [cmd.expr || ''], }); out = { id: cmd.id, ok: true, result: res && res[0] && res[0].result, tabId: tab.id, windowId: tab.windowId, url: tab.url };+ } else if (cmd.action === 'tile') {+ // Render the taskbar name tile HERE (OffscreenCanvas, extension-owned font): no page, no page CSP.+ const r = await self.pupRenderTile(cmd.name || '', chrome.runtime.getURL('fg-700.woff2'), cmd.sizes || [], cmd.render || 512);+ out = { id: cmd.id, ok: true, result: JSON.stringify(r) }; } 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.