← 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.

John Lauer ·a2714a984d ·1mo ago ·parent a4f0dc2
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.