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.558 (insiders): Extension-lane taskbar identity: late rebind covers unbound, dead-hwnd and unstamped ext windows;…
a2714a9
1mo ago
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
// 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 };
};