← Commit history

Dashboard hierarchy (John): render TASKBAR BUTTON -> window(s) -> tabs, with a favicon on every tab. Which windows share a taskbar button now depends on the real rule — taskbarGrouping 'split' gives each window its own AUMID and therefore its own button, 'grouped' stacks them under one Adom Pup button, and under minimalTouch pup stamps no identity so they are plain Chrome buttons — and the dashboard models that explicitly. This is also diagnostic: two sessions sharing one hwnd (the duplicate-session bug that split one PDF window's five tabs across five sessions) now show as two rows under a SINGLE button instead of looking like two unrelated windows. Each tab shows its own favicon via a new /dashboard/favicon endpoint, fetched through the page so cookie-protected app icons resolve, cached by tab URL so repeated SSE renders never re-fetch, and capped so the cache cannot grow unbounded.

John Lauer ·14bb89d935 ·3mo ago ·parent cb6cbd0
4 files changed +74−10
adom-bridge-puppeteer-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "1.9.224",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.224/adom-bridge-puppeteer-v1.9.224.zip",-  "sha256": "5b97f807b0da8c28b261dff119957eff9c1d362496e81fa063c6e5b1e4e6f4bb",-  "size": 451265,+  "version": "1.9.225",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.225/adom-bridge-puppeteer-v1.9.225.zip",+  "sha256": "a5f76515d371a34456bffb6a8abfbff0ef5c3ded20d9186e7407e1cd6644b184",+  "size": 452578,   "verbPrefixes": [     "browser_",     "desktop_recorder_",
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.224+1.9.225
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "1.9.224",+  "version": "1.9.225",   "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. browser_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. browser_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, browser_alert_window posts a native toast whose click focuses the window, browser_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-desktop",   "author": "Adom Inc.",
src/server.js+68−4
@@ -7748,6 +7748,7 @@ function ownershipRefusal(session, sessionId, args, verb) { // with NO CDP calls, so opening the dashboard can never wake a throttled page, disturb a window, or // add load to the shared socket. It reports what pup BELIEVES, which is exactly what you want when // debugging pup: a divergence between this and the screen IS the bug.+const _tabFavCache = new Map();   // tab URL -> {buf, mime} (v1.9.225) async function dashboardSnapshot() {   const now = Date.now();   const wins = [];@@ -7785,6 +7786,7 @@ async function dashboardSnapshot() {       tabCount: tabs.length,       tabs,       overlay: sess._overlayDesc || null,+      _curAppId: sess._curAppId || null,       hwnd: sess._hwnd || null,       emulatedViewport: sess._emulatedViewport || null,       lostBrowser: !!sess._lostBrowser,@@ -7794,6 +7796,22 @@ async function dashboardSnapshot() {       url: (() => { try { return sess.page && sess.page.url(); } catch (e) { return sess._lastUrl || null; } })(),     });   }+  // v1.9.225 (John): group into the HIERARCHY the user actually sees — TASKBAR BUTTON → window(s) →+  // tabs. Which windows share a button depends on pup's taskbarGrouping setting: 'split' gives each+  // window its own AUMID (its own button), 'grouped' stacks them all under one "Adom Pup" button, and+  // under minimalTouch pup stamps no identity at all so they are plain Chrome buttons. Modelling this+  // explicitly is also diagnostic: two sessions sharing one hwnd (the duplicate-session bug) show up+  // as two rows under a single button instead of looking like two unrelated windows.+  const grouping = (() => { try { return pupSettings().taskbarGrouping || 'split'; } catch (e) { return 'split'; } })();+  const buttons = new Map();+  for (const w of wins) {+    let key, label;+    if (minimalTouch()) { key = 'chrome:' + (w.hwnd || w.sessionId); label = 'Chrome (unbranded — minimal-touch)'; }+    else if (grouping === 'grouped') { key = 'Adom.Pup'; label = 'Adom Pup (grouped)'; }+    else { key = (w._curAppId || ('Adom.Pup.' + w.sessionId)); label = w.sessionId; }+    if (!buttons.has(key)) buttons.set(key, { key, label, grouping: minimalTouch() ? 'minimal-touch' : grouping, windows: [] });+    buttons.get(key).windows.push(w.sessionId);+  }   const threads = [...new Set(wins.map(w => w.owner || w.ownerThread).filter(Boolean))].sort();   const profiles = {};   for (const w of wins) profiles[w.profile] = (profiles[w.profile] || 0) + 1;@@ -7803,6 +7821,7 @@ async function dashboardSnapshot() {     bridge: { version: BRIDGE_VERSION, platform: process.platform, uptimeMs: Math.round(process.uptime() * 1000), depsOk, minimalTouch: minimalTouch() },     counts: { windows: wins.length, tabs: wins.reduce((a, w) => a + w.tabCount, 0), browsers: browsers.size, threads: threads.length },     threads, profiles,+    taskbarButtons: [...buttons.values()],     windows: wins.sort((a, b) => (b.openedAt || 0) - (a.openedAt || 0)),   }; }@@ -7824,7 +7843,14 @@ h1{font-size:15px;margin:0;font-weight:650;letter-spacing:.2px} .pill{background:#111b23;border:1px solid #1d2833;border-radius:999px;padding:3px 10px;font-size:12px;color:#9fb0c0} .pill b{color:#e6edf3;font-weight:600} select,input{background:#111b23;color:#e6edf3;border:1px solid #23303c;border-radius:6px;padding:5px 9px;font-size:13px}-main{padding:16px 18px;display:grid;gap:12px;grid-template-columns:repeat(auto-fill,minmax(430px,1fr))}+main{padding:16px 18px;display:flex;flex-direction:column;gap:16px}+.tbgroup{border:1px solid #23303c;border-radius:12px;background:#0c131a;overflow:hidden}+.tbhead{display:flex;gap:10px;align-items:center;padding:10px 14px;background:#111b23;border-bottom:1px solid #23303c}+.tbicon{width:30px;height:30px;border-radius:7px;background:#0b1116;flex:0 0 30px;object-fit:contain}+.tblabel{font-weight:650;font-size:14px}+.tbmeta{margin-left:auto;color:#7d8f9f;font-size:12px}+.tbgroup .card{margin:12px;border-radius:9px}+.fav{width:15px;height:15px;flex:0 0 15px;object-fit:contain;border-radius:3px;background:#0b1116} .card{background:#0f1720;border:1px solid #1d2833;border-radius:10px;overflow:hidden} .card.fg{border-color:#12e0c4;box-shadow:0 0 0 1px rgba(18,224,196,.25)} .card.crash{border-color:#f85149}@@ -7888,7 +7914,15 @@ function render(){     if(th&&(w.owner||w.ownerThread)!==th)return false;     if(q){const hay=(w.sessionId+' '+(w.url||'')+' '+(w.owner||'')+' '+w.tabs.map(t=>t.url||'').join(' ')).toLowerCase();if(!hay.includes(q))return false;}     return true;});-  $('#m').innerHTML = rows.length? rows.map(w=>{+  // v1.9.225 — render the HIERARCHY: taskbar button -> window(s) -> tabs.+  const byId=Object.fromEntries(rows.map(w=>[w.sessionId,w]));+  const groups=(d.taskbarButtons||[]).map(b=>({...b,wins:b.windows.map(s=>byId[s]).filter(Boolean)})).filter(g=>g.wins.length);+  $('#m').innerHTML = groups.length? groups.map(g=>+    '<div class="tbgroup">'+    +'<div class="tbhead"><img class="tbicon" src="/dashboard/badge?session='+encodeURIComponent(g.wins[0].sessionId)+'&t='+d.ts+'" onerror="this.style.visibility=\'hidden\'">'+      +'<span class="tblabel">'+esc(g.label)+'</span>'+      +'<span class="tbmeta">taskbar button · '+g.wins.length+' window'+(g.wins.length>1?'s':'')+' · '+esc(g.grouping)+'</span></div>'+    + g.wins.map(w=>{     const crashed=w.tabs.some(t=>t.crashed)||w.lostBrowser;     return '<div class="card '+(w.foreground?'fg':'')+(crashed?' crash':'')+'">'     +'<div class="chead">'@@ -7912,8 +7946,11 @@ function render(){       +'<tr><td class="k">hwnd</td><td>'+(w.hwnd||'<span style="color:#ff7b72">unresolved</span>')+'</td></tr>'       +(w.lastAgentUpdate?'<tr><td class="k">last action</td><td>'+esc(w.lastAgentUpdate.verb||'')+' '+esc(w.lastAgentUpdate.note||'')+'</td></tr>':'')     +'</table>'-    +'<div class="tabs">'+w.tabs.map(t=>'<div class="tab '+(t.active?'act':'')+'"><span class="id">'+esc(t.tabId)+'</span><span class="u">'+esc(t.url||'—')+'</span>'+(t.crashed?'<span class="x">crashed</span>':'')+'</div>').join('')+'</div>'-    +'</div>';}).join('') : '<div class="empty">no pup windows open'+(th||q?' matching this filter':'')+'</div>';+    +'<div class="tabs">'+w.tabs.map(t=>'<div class="tab '+(t.active?'act':'')+'">'+        +'<img class="fav" src="/dashboard/favicon?session='+encodeURIComponent(w.sessionId)+'&tab='+encodeURIComponent(t.tabId)+'" onerror="this.style.visibility=\'hidden\'">'+        +'<span class="id">'+esc(t.tabId)+'</span><span class="u">'+esc(t.title||t.url||'—')+'</span>'+        +(t.crashed?'<span class="x">crashed</span>':'')+'</div>').join('')+'</div>'+    +'</div>';}).join('')+'</div>').join('') : '<div class="empty">no pup windows open'+(th||q?' matching this filter':'')+'</div>';   $('#ft').textContent='bridge up '+ago(d.bridge.uptimeMs)+' · '+d.bridge.platform+' · deps '+(d.bridge.depsOk?'ok':'MISSING')+' · profiles: '+Object.entries(d.profiles).map(([k,v])=>k+'('+v+')').join(', '); } $('#f').onchange=render; $('#q').oninput=render;@@ -7964,6 +8001,33 @@ const server = http.createServer(async (req, res) => {       req.on('close', () => { alive = false; clearInterval(iv); });       return;     }+    if (pathname === '/dashboard/favicon') {+      // v1.9.225 (John): every TAB shows its own favicon in the dashboard as a visual cue. Cached per+      // tab URL so repeated SSE renders never re-fetch, and fetched through the page itself (cookies)+      // so authed app icons resolve — the same path that fixed the dashboard-v2 badge.+      const sid = url.searchParams.get('session') || '';+      const tid = url.searchParams.get('tab') || '';+      try {+        const sess = sessions.get(sid);+        const tab = sess && (sess.tabs || []).find(t => t.tabId === tid);+        if (!tab || !tab.page) { res.writeHead(404); res.end(); return; }+        let key = null; try { key = tab.page.url(); } catch (e) {}+        if (key && _tabFavCache.has(key)) {+          const c = _tabFavCache.get(key);+          res.writeHead(200, { 'Content-Type': c.mime || 'image/png', 'Cache-Control': 'no-store' });+          res.end(c.buf); return;+        }+        const src = await Promise.race([+          fetchPageFavicon(tab.page),+          new Promise(r => setTimeout(() => r(null), 3000)),+        ]);+        if (!src || !src.b64) { res.writeHead(404); res.end(); return; }+        const buf = Buffer.from(src.b64, 'base64');+        if (key) { _tabFavCache.set(key, { buf, mime: src.mime || 'image/png' }); if (_tabFavCache.size > 200) _tabFavCache.delete(_tabFavCache.keys().next().value); }+        res.writeHead(200, { 'Content-Type': src.mime || 'image/png', 'Cache-Control': 'no-store' });+        res.end(buf); return;+      } catch (e) { res.writeHead(404); res.end(); return; }+    }     if (pathname === '/dashboard/badge') {       // Serve the exact overlay PNG pup last composed for a session, so the dashboard shows the REAL       // badge rather than a re-render that might differ from what Windows was handed.