← Commit history

New Thread Tiles mode (Settings toggle threadTiles, off by default): group every pup window and tab of one ai-thread under ONE Windows taskbar button whose icon is that thread's NAME rendered on the Adom teal tile in Familjen Grotesk. The name tile is rendered by Chrome (the one renderer guaranteed to have the woff2 font) into a 256px PNG with auto-fit and a smart 2-line wrap, then wrapped into a multi-size .ico (256/48/32/16 via sharp) and cached once per thread name. The per-thread AUMID (Adom.Pup.thread.<slug>) takes precedence over category/grouped/split; both the registry IconResource and the per-window Relaunch icon use the tile so the button is born named; the taskbar hover label reads the thread name too.

John Lauer ·1ef409523c ·2mo ago ·parent 80f4376
4 files changed +144−12
adom-bridge-puppeteer-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "2.0.151",-  "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.151/adom-bridge-puppeteer-v2.0.151.zip",-  "sha256": "5d49788e9610db85a6b27bad0b88374264214727db4e76f1f60fd1a50e20818f",-  "size": 901842,+  "version": "2.0.152",+  "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.152/adom-bridge-puppeteer-v2.0.152.zip",+  "sha256": "6cc7bffb24860c63a48ed860e12133960532eebe79b415678ad6f0d3ce3da0c6",+  "size": 905342,   "verbPrefixes": [     "browser_",     "desktop_recorder_",
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-2.0.151+2.0.152
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "2.0.151",+  "version": "2.0.152",   "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+138−6
@@ -3477,7 +3477,12 @@ function pupSettings() {   // the _chosen tracking means flipping a default only moves users who never set the key.)   const D = { aumidIcons: 'on', jumpLists: 'on', fgWatch: 'on', fgCaption: 'on', fgCaptionMs: 2000,               overlayBadges: 'on', autoFlash: 'off', sleepRecovery: 'on', openFlash: 'off',-              aumidIconStyle: 'category', taskbarGrouping: 'split' };+              aumidIconStyle: 'category', taskbarGrouping: 'split',+              // v2.0.152 THREAD TILES (John): group every pup window/tab of one ai-thread under ONE+              // taskbar button whose icon is that thread's NAME rendered on the teal tile (Familjen+              // Grotesk, auto-fit). Off by default; when on it overrides the grouping so a thread's+              // whole pup estate lives under a single named tile ("oh, that's my shotlog thread").+              threadTiles: 'off' };   D.openFlash = 'on';   // v1.9.262 (John: "why did you default it to off when its defaulted to on")   if (_pupSettings.openFlash === 'off' && !_pupSettings._openFlashChosen) _pupSettings.openFlash = 'on';   // v1.9.268 (John on autoFlash: "this should be on by default")@@ -3994,7 +3999,13 @@ async function stampPupIdentity(sessionId, opts) {     // "standard" style still collapses to the single Adom.Pup button (one icon, so per-category buttons     // would be pointless). Split is unchanged (one button per window).     const _catGroup = grouped && pupSettings().aumidIconStyle === 'category';+    // v2.0.152 THREAD TILES: when on, every window of an ai-thread shares ONE AUMID keyed on the+    // thread name, so the whole thread collapses to a single named taskbar button. Takes precedence+    // over category/grouped/split. The dashboard keeps its own identity; a window with no owner+    // thread falls through to the normal modes.+    const _thread = (!_dashInfra && pupSettings().threadTiles === 'on' && sess && (sess.owner || sess._ownerThread)) || null;     let appId = _dashInfra ? `${PUP_APP_ID}.Dashboard.${PUP_ICON_GEN}`+      : _thread ? `${PUP_APP_ID}.thread.${threadSlug(_thread)}.${THREAD_TILE_GEN}`       : _catGroup ? `${PUP_APP_ID}.${cat}.${PUP_ICON_GEN}`       : grouped ? PUP_APP_ID       : `${PUP_APP_ID}.${sessionId}.${cat}.${PUP_ICON_GEN}`;@@ -4006,7 +4017,7 @@ async function stampPupIdentity(sessionId, opts) {     // for ZERO taskbar churn for the window's life. Grouped/dashboard keep their own identities; the     // active-tab favicon overlay still updates (that is the point, not churn), and the jump-list still     // follows a view (public<->authed) flip since that changes the menu, not the button.-    if (sess && sess._frozenAppId && !_dashInfra && !grouped) appId = sess._frozenAppId;+    if (sess && sess._frozenAppId && !_dashInfra && !grouped && !_thread) appId = sess._frozenAppId;     const prevAppId = sess && sess._curAppId;     // v1.9.279 THE IDEMPOTENCY GATE: only touch the window when its identity actually changes.     // desktop_set_window_identity RECREATES the taskbar button every call, so firing it when nothing@@ -4054,10 +4065,20 @@ async function stampPupIdentity(sessionId, opts) {     // Title stays as a fallback only for a window we somehow have no hwnd for.     if (sess && !sess._hwnd) { try { await resolveSessionHwndByBounds(sess, sessionId); } catch (e) {} }     const _stampHwnd = (sess && sess._hwnd && await hwndBelongsToPup(sess._hwnd, sess)) ? sess._hwnd : null;+    // v2.0.152 THREAD TILES: the per-window Relaunch icon must match the thread tile too, or Windows+    // would paint the default pup icon on the born button (it prefers the window's Relaunch icon over+    // the registry one). registerPupAumid ran just above and cached the .ico, so read it synchronously.+    let _stampIcon = pupIconPath(sessionId), _stampName = pupDisplayName(sessionId), _cacheKey = `pup-${sessionId}-${cat}`;+    if (appId.startsWith(`${PUP_APP_ID}.thread.`)) {+      const _tn = sess && (sess.owner || sess._ownerThread);+      const _tp = _tn ? path.join(THREAD_TILE_DIR, `${threadSlug(_tn)}.ico`) : null;+      try { if (_tp && fs.existsSync(_tp)) { _stampIcon = _tp; _stampName = String(_tn); } } catch (e) {}+      if (_tn) _cacheKey = `pup-thread-${threadSlug(_tn)}`;+    }     const r = await chrome.abCommand('desktop_set_window_identity', {       ...(_stampHwnd ? { hwnd: _stampHwnd } : {}),-      titleContains: `(${TITLE_LABEL}: ${sessionId}`, cacheKey: `pup-${sessionId}-${cat}`,-      appId, displayName: pupDisplayName(sessionId), iconPath: pupIconPath(sessionId),+      titleContains: `(${TITLE_LABEL}: ${sessionId}`, cacheKey: _cacheKey,+      appId, displayName: _stampName, iconPath: _stampIcon,       // v1.9.11 (AD ≥1.9.141): brand the taskbar RIGHT-CLICK MENU HEADER row (it showed the       // chrome.exe icon for months). There is NO "ApplicationIcon" registry value on AUMID keys       // and the registry cannot brand the header at all — the header obeys the window's@@ -4140,12 +4161,30 @@ const _registeredPupAumids = new Set(); async function registerPupAumid(appId, sessionId) {   if (minimalTouch()) return false;   // v1.9.190: minimal-touch — no taskbar work at all   if (_identitySupported === false || appId === PUP_APP_ID || _registeredPupAumids.has(appId)) return;+  // v2.0.152 THREAD TILES: a thread AUMID (Adom.Pup.thread.<slug>.<gen>) wears the thread's NAME tile+  // instead of the standard/category icon. Render it (once per thread, cached) from a live browser,+  // and fall back to the normal pup icon if the render fails so the button is never left unbranded.+  let iconPath = pupIconPath(sessionId);+  let displayName = pupDisplayName(sessionId);+  const _threadAumid = appId.startsWith(`${PUP_APP_ID}.thread.`);+  if (_threadAumid) {+    try {+      const s = sessions.get(sessionId);+      const threadName = s && (s.owner || s._ownerThread);+      const be = s && browsers.get(s.profileName);+      if (threadName && be && be.browser) {+        const tile = await renderThreadTileIco(be.browser, threadName);+        if (tile) iconPath = tile;+        displayName = String(threadName);   // the taskbar hover/label reads the thread name too+      }+    } catch (e) {}+  }   // v1.9.44: write IconResource FIRST, before the button can possibly exist. AD's registration does   // not write it (see writeAumidIcon), and an AUMID with no icon is what paints white.-  writeAumidIcon(appId, pupIconPath(sessionId));+  writeAumidIcon(appId, iconPath);   try {     const r = await chrome.abCommand('desktop_register_app_identity', {-      appId, displayName: pupDisplayName(sessionId), iconPath: pupIconPath(sessionId), shortcut: false,+      appId, displayName, iconPath, shortcut: false,     }, { timeoutMs: 6000 });     const raw = r && (typeof r.output === 'string' ? r.output : JSON.stringify(r.output || ''));     if (_isUnknownVerb(r, raw)) return; // older AD without #207 — the per-window stamp still brands the taskbar button@@ -4185,6 +4224,98 @@ function writeAumidIcon(appId, iconPath) {     return true;   } catch (e) { console.log(`[identity] IconResource write failed for ${appId}: ${e.message}`); return false; } }++// ── THREAD TILES (v2.0.152, John) ───────────────────────────────────────────────────────────────+// Group every pup window/tab of one ai-thread under ONE taskbar button whose icon is that thread's+// NAME rendered on the teal tile. The name is rendered in the real Adom font (Familjen Grotesk) by+// CHROME — the ONE renderer guaranteed to have the woff2 — into a 256px PNG, which sharp downscales+// to a multi-size .ico. Rendered once per thread name and cached on disk.+const THREAD_TILE_GEN = 't1';+const THREAD_TILE_DIR = path.join(os.tmpdir(), 'pup-thread-tiles', THREAD_TILE_GEN);+let _fgFontB64 = null;+function fgFontB64() {+  if (_fgFontB64 == null) { try { _fgFontB64 = fs.readFileSync(path.join(__dirname, 'extension', 'fg-700.woff2')).toString('base64'); } catch (e) { _fgFontB64 = ''; } }+  return _fgFontB64;+}+// AUMID- and filename-safe slug of an ai-thread name. Keep it stable + collision-resistant: a short+// hash suffix so "ah build" and "ah-build" never share a tile/button.+function threadSlug(name) {+  const s = String(name || 'thread').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 24) || 'thread';+  const h = require('crypto').createHash('sha1').update(String(name || '')).digest('hex').slice(0, 6);+  return `${s}-${h}`;+}+function threadTileHtml(name) {+  const font = fgFontB64();+  const face = font ? `@font-face{font-family:FG;src:url(data:font/woff2;base64,${font}) format('woff2');font-weight:700}` : '';+  const nm = String(name || '').replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));+  // Render at 256; the tile fills the whole canvas (Windows rounds the taskbar itself). Text is+  // #05221f on #00b8b1 (Adom teal), auto-fit with a smart 2-line wrap at a space/hyphen/mid-split.+  return `<!doctype html><meta charset=utf8><style>${face}+html,body{margin:0}#c{width:256px;height:256px;background:#00b8b1;display:flex;align-items:center;justify-content:center;overflow:hidden}+#t{color:#05221f;font-family:FG,system-ui,sans-serif;font-weight:700;line-height:.9;text-align:center;white-space:pre;letter-spacing:-.02em}</style>+<div id=c><div id=t></div></div><script>+(function(){var name=${JSON.stringify(String(name || ''))},box=256,el=document.getElementById('t');+function fs(text){var t=document.createElement('div');t.style.cssText='position:absolute;visibility:hidden;font-family:FG,system-ui;font-weight:700;line-height:.9;white-space:pre;letter-spacing:-.02em';t.textContent=text;document.body.appendChild(t);var s=box*0.92;t.style.fontSize=s+'px';for(var i=0;i<90&&(t.scrollWidth>box*0.86||t.scrollHeight>box*0.86);i++){s*=0.95;t.style.fontSize=s+'px';}t.remove();return s;}+var cands=[name];var sp=name.lastIndexOf(' '),hy=name.indexOf('-');+if(sp>0)cands.push(name.slice(0,sp)+'\\n'+name.slice(sp+1));else if(hy>0)cands.push(name.slice(0,hy+1)+'\\n'+name.slice(hy+1));+if(sp<0&&hy<0&&name.length>=7){var m=Math.ceil(name.length/2);cands.push(name.slice(0,m)+'\\n'+name.slice(m));}+var best=name,bf=0;cands.forEach(function(x){var f=fs(x);if(f>bf){bf=f;best=x;}});+el.textContent=best;el.style.fontSize=bf+'px';document.fonts&&document.fonts.ready&&document.fonts.ready.then(function(){var f=fs(best);el.style.fontSize=f+'px';el.textContent=best;});+window.__tileReady=true;})();+</script>`;+}+// Wrap a 256x256 PNG buffer into a multi-size .ico (256/48/32/16, each a PNG-compressed entry — the+// modern ICO form Windows Vista+ reads). sharp does the downscales (available on the desktop).+async function pngToIco(png256) {+  let sharp; try { sharp = require('sharp'); } catch (e) { return null; }+  const sizes = [256, 48, 32, 16];+  const pngs = [];+  for (const s of sizes) {+    const b = s === 256 ? png256 : await sharp(png256).resize(s, s, { fit: 'fill', kernel: 'lanczos3' }).png().toBuffer();+    pngs.push({ s, b });+  }+  const N = pngs.length;+  const header = Buffer.alloc(6); header.writeUInt16LE(0, 0); header.writeUInt16LE(1, 2); header.writeUInt16LE(N, 4);+  const dir = Buffer.alloc(16 * N);+  let offset = 6 + 16 * N;+  pngs.forEach((p, i) => {+    const e = dir.subarray(i * 16, i * 16 + 16);+    e.writeUInt8(p.s >= 256 ? 0 : p.s, 0); e.writeUInt8(p.s >= 256 ? 0 : p.s, 1);+    e.writeUInt8(0, 2); e.writeUInt8(0, 3); e.writeUInt16LE(1, 4); e.writeUInt16LE(32, 6);+    e.writeUInt32LE(p.b.length, 8); e.writeUInt32LE(offset, 12); offset += p.b.length;+  });+  return Buffer.concat([header, dir, ...pngs.map(p => p.b)]);+}+const _threadTileInflight = new Map();   // name -> Promise (dedupe concurrent renders)+// Render (or return cached) the .ico path for a thread name. `browser` is any live puppeteer Browser+// (we open a throwaway 256x256 background page, screenshot the tile, close it). Cached on disk.+async function renderThreadTileIco(browser, name) {+  const icoPath = path.join(THREAD_TILE_DIR, `${threadSlug(name)}.ico`);+  try { if (fs.existsSync(icoPath) && fs.statSync(icoPath).size > 100) return icoPath; } catch (e) {}+  if (_threadTileInflight.has(name)) return _threadTileInflight.get(name);+  const job = (async () => {+    if (!browser) return null;+    let page = null;+    try {+      fs.mkdirSync(THREAD_TILE_DIR, { recursive: true });+      page = await browser.newPage();+      await page.setViewport({ width: 256, height: 256, deviceScaleFactor: 1 });+      await page.goto('data:text/html;base64,' + Buffer.from(threadTileHtml(name)).toString('base64'), { waitUntil: 'domcontentloaded', timeout: 8000 });+      try { await page.waitForFunction('window.__tileReady === true', { timeout: 3000 }); } catch (e) {}+      try { await page.evaluate(() => document.fonts && document.fonts.ready); } catch (e) {}+      await new Promise(r => setTimeout(r, 120));   // let the post-font re-fit settle+      const png = await page.screenshot({ clip: { x: 0, y: 0, width: 256, height: 256 }, type: 'png' });+      const ico = await pngToIco(png);+      if (!ico) return null;+      fs.writeFileSync(icoPath, ico);+      console.log(`[thread-tile] rendered tile for "${name}" -> ${icoPath} (${ico.length}B)`);+      return icoPath;+    } catch (e) { console.log(`[thread-tile] render failed for "${name}": ${e.message}`); return null; }+    finally { try { if (page) await page.close({ runBeforeUnload: false }); } catch (e) {} _threadTileInflight.delete(name); }+  })();+  _threadTileInflight.set(name, job);+  return job;+} // v1.9.44: the AUMID keys LEAKED. A new key is minted per session per category, but only the // immediately-superseded one is unregistered, and only in-process — so every bridge restart orphaned // its whole set (129 keys had accumulated). Sweep on startup: drop any Adom.Pup.<sid>.* key whose@@ -11397,6 +11528,7 @@ const server = http.createServer(async (req, res) => {             reportIssues: ['on', 'off'], autoLogin: ['on', 'off', 'submit', 'fill'], captureLogins: ['on', 'off'], minimalTouch: ['on', 'off'],             taskbarGrouping: ['split', 'grouped'],             aumidIconStyle: ['standard', 'category'],+            threadTiles: ['on', 'off'],             jlViewTasks: ['on', 'off'], jlCloseAll: ['on', 'off'], jlDashboard: ['on', 'off'], jlAnnotate: ['on', 'off'], annCaptionBar: ['on', 'off'], toolbarExtension: ['on', 'off'], tbOwnerBadge: ['on', 'off'], tbSnipHotkey: ['on', 'off'], tbFloatyPencil: ['on', 'off'],             notMineCaption: ['on', 'off'], ownerChip: ['on', 'off'],             titleTag: ['on', 'off'], titlePrefix: ['on', 'off'],