← Commit history

The pup settings dialog (John: 'we need a settings dialog in the dashboard now', + 'if there are other prefs i forgot about, add those too', + the orange-hilite toggle). A gear button in the dashboard header opens a modal with FOURTEEN live preferences, each wired to a real behavior gate and persisted across bridge updates: (1) AUMID taskbar icons, default OFF - John's EXPLICIT experiment re-decision per the dev-skills/pup-aumid-decisions process rule ('then i can play around with having it on or off and see if aumid is what is crashing the app or not'); on = one gentle stamp per window at creation, each window its own icon, with the split/grouped sub-select enabled only when on; (2) jump lists, default OFF; (3) the 20s foreground watch (no-fault caption), default on; (4) caption-on-AI-foreground, default ON, with a sub-slider for duration (0.5s to 5s, default 2s) that enables/disables with its toggle - a USER-chosen duration may exceed the AI 2s anti-rudeness cap because that cap exists to stop the AI, not the user; (5) favicon overlay badges; (6) orange highlight on new-window open, default off; (7) flash on agent activity; (8) taskbar progress bar; (9) sleep/crash recovery; (10) auto-fill saved logins; (11) issue-report hints; (12) minimal-touch master mode; plus grouping and duration as dependent sub-controls. Server side: whitelisted set-setting action with validation, settings ride the SSE snapshot, every change answers back with a toast. Toggles follow the adom-ui-design toggle rules (role=switch, aria-checked, two visual differences between states). Ralph grew 4 effect checks: roundtrip persistence, unknown-key rejection, dialog renders all controls, and the slider disables when its parent toggle turns off.

John Lauer ·9c534a5cda ·2mo ago ·parent 08daf17
4 files changed +148−24
adom-bridge-puppeteer-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "1.9.258",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.258/adom-bridge-puppeteer-v1.9.258.zip",-  "sha256": "df1c5d385d651ff2a6dd5e3ed4eafaf54845e02232a76f890a1d032e81b20f50",-  "size": 472762,+  "version": "1.9.259",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.259/adom-bridge-puppeteer-v1.9.259.zip",+  "sha256": "047ada481a4ff23703324a720883b35bcf58ed2e84195e9474a72f588758fe7a",+  "size": 476236,   "verbPrefixes": [     "browser_",     "desktop_recorder_",
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.258+1.9.259
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "1.9.258",+  "version": "1.9.259",   "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+142−18
@@ -1942,8 +1942,11 @@ async function ensureSafeForNativeInput(sessionId, { reason = 'drive the window // can violate it regardless of what it requests. A caption is a glance, not a dialog: if something // genuinely needs the user's attention for longer, use a toast (notify_user), which they dismiss. const PUP_CAPTION_MAX_MS = 2000;-async function pupCaption(text, expiresInMs = PUP_CAPTION_MAX_MS, id = 'pup-assist') {-  expiresInMs = Math.min(Number(expiresInMs) || PUP_CAPTION_MAX_MS, PUP_CAPTION_MAX_MS);+async function pupCaption(text, expiresInMs = PUP_CAPTION_MAX_MS, id = 'pup-assist', maxMs) {+  // maxMs: a USER-CHOSEN duration (settings slider) may exceed the 2s anti-rudeness cap — the cap+  // exists to stop the AI outstaying its welcome, not to override the user's own preference.+  const _cap = Number(maxMs) > 0 ? Number(maxMs) : PUP_CAPTION_MAX_MS;+  expiresInMs = Math.min(Number(expiresInMs) || _cap, _cap);   // v1.9.244 (John: "you left the caption up forever. i've told you 100 times: 2 seconds"). The 2s   // clamp NEVER REACHED AD: current AD's desktop_caption takes `duration`, pup sent `expiresInMs`   // (an older AD spelling), so AD ignored it and used its 30s default. Send BOTH names so every AD@@ -2705,6 +2708,10 @@ function pupSettings() {   if (_pupSettings) return _pupSettings;   try { _pupSettings = JSON.parse(fs.readFileSync(PUP_SETTINGS_PATH, 'utf8')); } catch (e) { _pupSettings = {}; }   if (!_pupSettings.taskbarGrouping) _pupSettings.taskbarGrouping = 'split';+  // v1.9.259 (John's settings dialog): every user-facing behavior gets an explicit pref.+  const D = { aumidIcons: 'off', jumpLists: 'off', fgWatch: 'on', fgCaption: 'on', fgCaptionMs: 2000,+              overlayBadges: 'on', autoFlash: 'off', sleepRecovery: 'on', openFlash: 'off' };+  for (const k of Object.keys(D)) if (_pupSettings[k] === undefined) _pupSettings[k] = D[k];   return _pupSettings; } function savePupSettings(patch) {@@ -2884,12 +2891,17 @@ async function stampPupIdentity(sessionId, opts) {   // v1.9.185 minimal-touch: the AUMID stamp is the single most invasive thing pup does to a window   // it does not own (Windows DESTROYS and RECREATES the taskbar button on an AppUserModelID change).   // Gated at the choke point so no caller can leak it.-  // v1.9.248 REVERTED: v1.9.245 re-enabled stamping here off an ambiguous question, silently-  // reversing the settled 2026-07-29 NO-AUMID decision (John: "we just spent 2 days having you stop-  // using aumid and you just started using aumid again?"). The decision STANDS: overlays only,-  // no identity stamps, in any mode. Full history + the process rule for ever revisiting this:-  // dev-skills/pup-aumid-decisions/SKILL.md. Do NOT add an escape hatch here again.-  if (minimalTouch()) return false;+  // v1.9.248 reverted the silent 1.9.245 re-enable (see dev-skills/pup-aumid-decisions).+  // v1.9.259: John EXPLICITLY re-decided, per that skill's process rule, as a USER-CONTROLLED+  // EXPERIMENT toggle (2026-08-01: "a key setting is letting me turn on AUMID taskbar icons or+  // leaving them off... then i can play around with having it on or off and see if aumid is what's+  // crashing the app or not"). Default stays OFF. When the user turns it on: ONE gentle stamp per+  // window at creation ({once}), never the category re-stamp churn.+  if (minimalTouch()) {+    if (pupSettings().aumidIcons !== 'on' || !(opts && opts.once)) return false;+    const _s0 = sessions.get(sessionId);+    if (_s0 && _s0._curAppId) return true;+  }   if (process.platform !== 'win32' || !sessionId || _identitySupported === false) return false;   if (!_identityRegistered) { await ensurePupIdentityRegistered(); if (_identitySupported === false) return false; }   try {@@ -2949,7 +2961,7 @@ async function stampPupIdentity(sessionId, opts) {       // the header baked GENERIC and the later headerBranded:true had no visible effect — exactly what       // John kept seeing. Brand the header (writes the window's Relaunch trio) FIRST, so the button is       // born with it.-      if (!minimalTouch()) { try { await updateWikiJumplist(sess, sessionId, { appId }); } catch (e) {} }+      if (pupSettings().jumpLists === 'on') { try { await updateWikiJumplist(sess, sessionId, { appId }); } catch (e) {} }     }     const r = await chrome.adCommand('desktop_set_window_identity', {       titleContains: `(session: ${sessionId}`, cacheKey: `pup-${sessionId}-${cat}`,@@ -2985,7 +2997,7 @@ async function stampPupIdentity(sessionId, opts) {         sess._appOverlay = false; sess._overlayEnforceAt = 0; sess._hwnd = null;         // The jump list attaches to the AUMID — a new appId starts with NO jump list, so force a         // re-attach on the next updateWikiJumplist pass.-        if (!minimalTouch()) updateWikiJumplist(sess, sessionId).catch(() => {});   // deduped by _wikiJumplistKey+        if (pupSettings().jumpLists === 'on') updateWikiJumplist(sess, sessionId).catch(() => {});   // deduped by _wikiJumplistKey         // v1.9.112 (John: "still shows for 200ms then disappears; second right-click works") —         // RE-COMMIT THE JUMP LIST ONCE THE BUTTON IS REAL. Measured elimination got us here: nothing         // fires while idle (0 log lines in 40s), and windows with exactly ONE AUMID registration blip@@ -3384,10 +3396,13 @@ const _fgToastAt = new Map(); // arrives, so the justification is accountable in real time. Deliberately NOT gated by minimalTouch: // this is a user-protection signal, not cosmetic machinery. async function captionForegroundReason(sessionId, reason, verb) {+  // v1.9.259: user prefs — toggle + duration slider (500..5000ms, default 2000).+  if (pupSettings().fgCaption !== 'on') return;+  const _capMs = Math.max(500, Math.min(5000, Number(pupSettings().fgCaptionMs) || 2000));   try {     const why = String(reason || '').trim();     if (!why) return;-    await pupCaption(`Adom brought "${sessionId}" to the front. Reason: "${why}"`, 2000, 'pup-fg-reason');+    await pupCaption(`Adom brought "${sessionId}" to the front. Reason: "${why}"`, _capMs, 'pup-fg-reason', _capMs);   } catch (e) {} } @@ -4119,6 +4134,7 @@ async function applyAppOverlay(session, sessionId) {   // the old branding so invasive on SPAs that re-registered constantly). So in minimal mode pup   // stamps NO identity and rebuilds NO jump list, but it DOES wear one composed badge per window.   if (process.platform !== 'win32' || _identitySupported !== true) return false;+  if (pupSettings().overlayBadges !== 'on') return false;   // v1.9.259 user pref   try {     // v1.9.0: reflect the ACTIVE tab (John: favicons are more useful than counts — show what     // the user is looking at). Falls back to session.page.@@ -6068,7 +6084,7 @@ const _flashState = new Map(); // sessionId -> last flash ts const _openFlashDone = new Map(); // sessionId -> ts of the launch flash for the current open function scheduleAutoFlash(session, opts) {   opts = opts || {};-  if (minimalTouch()) return;   // v1.9.185: no per-verb taskbar flashing in minimal mode+  if (pupSettings().autoFlash !== 'on') return;   // v1.9.259: explicit pref (default off = old minimal-touch behavior)   if (process.platform !== 'win32' || !session || !session._sessionId) return;   if (opts.silent || session._flashSilent) return;   const key = session._sessionId;@@ -6860,7 +6876,7 @@ async function launchSession(sessionId, url, { freshProfile = false, profile, st   // v1.9.185 minimal-touch: do NOT stamp a per-session AUMID. Changing a window's AppUserModelID   // makes Windows DESTROY and RECREATE its taskbar button — the single most invasive thing pup did   // to a window it does not own. In minimal mode the window keeps Chrome's own identity.-  if (process.platform === 'win32' && !minimalTouch()) { stampPupIdentity(sessionId).catch(() => {}); }+  if (process.platform === 'win32' && (!minimalTouch() || pupSettings().aumidIcons === 'on')) { stampPupIdentity(sessionId, { once: true }).catch(() => {}); }    // v1.8.68: the park is DEFINED here but KICKED OFF only after the real URL's navigation has   // SETTLED (below). Rationale: the first cross-origin paint RAISES the window; if the window@@ -6910,7 +6926,7 @@ async function launchSession(sessionId, url, { freshProfile = false, profile, st   // the open handler's later scheduleAutoFlash debounces (no double flash). Fire-and-forget.   if (!foreground && !silent && process.platform === 'win32') {     // v1.9.185 minimal-touch: no taskbar flash at all (and no log noise pretending one fired).-    if (!minimalTouch()) { try { _flashState.set(sessionId, Date.now()); _openFlashDone.set(sessionId, Date.now()); console.log(`[flash] "${sessionId}" (launch)`); flashViaAD(sessionId, {}); } catch (e) {} }+    if (pupSettings().openFlash === 'on') { try { _flashState.set(sessionId, Date.now()); _openFlashDone.set(sessionId, Date.now()); console.log(`[flash] "${sessionId}" (launch)`); flashViaAD(sessionId, { force: true }); } catch (e) {} }   }    if (url) {@@ -8174,7 +8190,7 @@ async function dashboardSnapshot() {   let depsOk = false; try { require.resolve('puppeteer'); depsOk = true; } catch (e) {}   return {     ts: now,-    bridge: { version: BRIDGE_VERSION, platform: process.platform, uptimeMs: Math.round(process.uptime() * 1000), depsOk, minimalTouch: minimalTouch(), flashedPending: _flashedHwnds.size },+    bridge: { version: BRIDGE_VERSION, platform: process.platform, uptimeMs: Math.round(process.uptime() * 1000), depsOk, minimalTouch: minimalTouch(), flashedPending: _flashedHwnds.size, settings: pupSettings() },     counts: { windows: wins.length, tabs: wins.reduce((a, w) => a + w.tabCount, 0), browsers: browsers.size, threads: threads.length },     threads, profiles,     taskbarButtons: [...buttons.values()],@@ -8232,6 +8248,25 @@ select,input{background:#111b23;color:#e6edf3;border:1px solid #23303c;border-ra .toast.success{border-left-color:#3fb950} .toast.error{border-left-color:#f85149} .toast.out{opacity:0}+#dlgWrap{position:fixed;inset:0;z-index:99998}+#dlgBg{position:absolute;inset:0;background:rgba(0,0,0,.55)}+#dlg{position:absolute;top:60px;left:50%;transform:translateX(-50%);width:min(560px,92vw);max-height:calc(100vh - 120px);overflow:auto;background:#161b22;border:1px solid #30363d;border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.6)}+.dlghead{position:sticky;top:0;display:flex;align-items:center;justify-content:space-between;padding:10px 16px;background:#161b22;border-bottom:1px solid #30363d;font-family:"Familjen Grotesk",Satoshi,sans-serif;font-weight:600;color:#e6edf3}+#dlgBody{padding:6px 16px 16px}+.srow{display:flex;align-items:flex-start;gap:12px;padding:11px 0;border-bottom:1px solid #21262d}+.srow:last-child{border-bottom:none}+.srow .stxt{flex:1 1 auto;min-width:0}+.srow .slbl{color:#e6edf3;font-weight:600;font-size:13px}+.srow .sdesc{color:#8b949e;font-size:12px;line-height:1.5;margin-top:2px}+.tgl{flex:0 0 auto;width:40px;height:22px;border-radius:999px;background:#21262d;border:1px solid #30363d;position:relative;cursor:pointer;transition:background .15s ease;margin-top:2px}+.tgl::after{content:"";position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:#8b949e;transition:left .15s ease,background .15s ease}+.tgl.is-on{background:#00b8b1;border-color:#00b8b1}+.tgl.is-on::after{left:20px;background:#05221f}+.sub{margin-left:26px;padding:8px 0 2px;display:flex;align-items:center;gap:10px}+.sub.sdis{opacity:.4;pointer-events:none}+.sub input[type=range]{flex:1 1 auto;accent-color:#00b8b1}+.sub .sval{color:#c9d1d9;font-family:"JetBrains Mono",monospace;font-size:12px;min-width:44px;text-align:right}+.sub select{background:#0d1117;color:#c9d1d9;border:1px solid #30363d;border-radius:6px;padding:4px 8px} #taskbar{position:sticky;top:44px;z-index:9;display:flex;gap:6px;align-items:center;padding:8px 18px;background:#0a0f14;border-bottom:1px solid #1d2833;overflow-x:auto} #taskbar .lbl{color:#5c6c7a;font-size:11px;margin-right:6px;flex:0 0 auto} .tbi{position:relative;width:48px;height:42px;border-radius:8px;background:#0f1720;border:1px solid #1d2833;display:flex;align-items:center;justify-content:center;cursor:pointer;flex:0 0 auto}@@ -8307,6 +8342,7 @@ footer{color:#5c6c7a;font-size:11px;padding:8px 18px 22px}     <span class="smeta"><b id="cW">0</b> windows &middot; <b id="cT">0</b> tabs &middot; <b id="cB">0</b> icons &middot; <span id="cLed" title="green: every taskbar button wears its real page-favicon overlay and no untracked pup windows exist. red: a window is bare or unmodeled."><span class="led red" id="cLedDot"></span> <b id="cLedTxt">?</b></span> &middot; <span id="age"></span></span>   </div>   <div class="acts2">+    <button class="ibtn" id="gear" title="pup settings" aria-label="settings"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h0a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h0a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v0a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></button>     <button class="ibtn" data-act="flash-all" title="flash every pup taskbar button" aria-label="flash all"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg></button>     <button class="ibtn" data-act="unflash-all" title="clear every flash" aria-label="clear flashes"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/><line x1="3" y1="3" x2="21" y2="21"/></svg></button>     <button class="ibtn danger" data-act="close-all" data-confirm="Close ALL pup windows?" title="close every pup window" aria-label="close all windows"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>@@ -8318,6 +8354,13 @@ footer{color:#5c6c7a;font-size:11px;padding:8px 18px 22px}   <input id="q" placeholder="filter url / session..." size="20"> </div> <div id="toasts"></div>+<div id="dlgWrap" class="hidden">+  <div id="dlgBg"></div>+  <div id="dlg" role="dialog" aria-label="pup settings">+    <div class="dlghead">pup settings<button class="ibtn" id="dlgClose" title="close settings" aria-label="close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div>+    <div id="dlgBody"></div>+  </div>+</div> <div id="taskbar"></div> <main id="m"><div class="empty">connecting...</div></main> <footer id="ft"></footer>@@ -8441,7 +8484,7 @@ function toast(msg,type){   _q('#toasts').appendChild(t);   setTimeout(()=>{t.classList.add('out');setTimeout(()=>t.remove(),350)},type==='error'?8000:4000); }-const ACTMSG={'flash':'flashed','unflash':'flash cleared on','flash-all':'flashed every pup taskbar button','unflash-all':'cleared all flashes','close-all':'closed all pup windows','close-window':'closed','close-tab':'closed tab in','fg':'bringing to front:','bg':'sent to background:'};+const ACTMSG={'flash':'flashed','unflash':'flash cleared on','flash-all':'flashed every pup taskbar button','unflash-all':'cleared all flashes','close-all':'closed all pup windows','close-window':'closed','close-tab':'closed tab in','fg':'bringing to front:','bg':'sent to background:','set-setting':'setting saved'}; async function act(p){   try{     const r=await fetch('/dashboard/action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(p)});@@ -8453,6 +8496,64 @@ async function act(p){   }catch(e){toast(p.action+' failed: '+e.message,'error');} } let SEENTOAST=0;+// v1.9.259: the settings dialog. Rows defined once; state comes from SNAP.bridge.settings; every+// change POSTs set-setting and answers back with a toast.+const SETTINGS_DEF=[+ {k:'aumidIcons',l:'AUMID taskbar icons',d:'EXPERIMENT: give every pup window its OWN taskbar icon by stamping a per-window identity. Toggle on/off to test whether AUMID stamping is what crashes Chrome. Off = all windows share one Chrome button (current mode). Already-stamped windows keep their icon until closed.'},+ {k:'taskbarGrouping',l:'AUMID grouping',d:'Only applies when AUMID icons are on: split = one icon per window; grouped = all pup windows under one Adom Pup icon.',sel:['split','grouped'],dep:'aumidIcons'},+ {k:'jumpLists',l:'Jump lists',d:'Build right-click jump-list menus on pup taskbar icons. Known to blip on Windows 11; off by default.'},+ {k:'fgWatch',l:'20s foreground watch',d:'For 20s after a window opens or navigates in the background, watch whether something (JavaScript, Chrome, a focus change) brings it to the foreground, and caption that it was not Adom. Never moves the window.'},+ {k:'fgCaption',l:'Caption on AI foreground',d:'When an AI brings a window to your screen, show its stated reason as an on-screen caption. The AI is always forced to give a reason; this shows it to you.'},+ {k:'fgCaptionMs',l:'',d:'',slider:[500,5000,250],dep:'fgCaption'},+ {k:'overlayBadges',l:'Favicon overlay badges',d:'Paint each pup taskbar icon with the favicon of its active tab plus the Adom mark.'},+ {k:'openFlash',l:'Orange highlight on new window',d:'Flash the taskbar button orange when a new pup window opens, so you can spot it.'},+ {k:'autoFlash',l:'Flash on agent activity',d:'Flash a window taskbar button when an AI changes that window (navigate, click, type). One flash per burst.'},+ {k:'agentActivity',l:'Taskbar progress bar',d:'Show a progress strip on a window taskbar button while an AI is actively driving it.'},+ {k:'sleepRecovery',l:'Sleep and crash recovery',d:'After the laptop wakes or Chrome dies, automatically bring every pup window back at its last page, signed in. Never restarts a live Chrome.'},+ {k:'autoLogin',l:'Auto-fill saved logins',d:'Fill (never submit) a matching saved login when a login page loads in a pup window.'},+ {k:'reportIssues',l:'Issue-report hints',d:'Let pup invite AIs to file a wiki issue when they hit something broken.'},+ {k:'minimalTouch',l:'Minimal-touch mode',d:'Master switch: pup does nothing to windows beyond opening and driving them. Individual toggles above override where they say so. Changing this affects many behaviors; leave on unless testing.'},+];+function renderDlg(){+  const st=(SNAP&&SNAP.bridge&&SNAP.bridge.settings)||{};+  _q('#dlgBody').innerHTML=SETTINGS_DEF.map(def=>{+    if(def.slider){+      const on=st[def.dep]==='on';+      const v=Number(st[def.k])||2000;+      return '<div class="sub'+(on?'':' sdis')+'" id="row-'+def.k+'"><span class="sdesc">caption duration</span>'+        +'<input type="range" min="'+def.slider[0]+'" max="'+def.slider[1]+'" step="'+def.slider[2]+'" value="'+v+'" data-slider="'+def.k+'" aria-label="caption duration">'+        +'<span class="sval" id="val-'+def.k+'">'+(v/1000).toFixed(1)+'s</span></div>';+    }+    if(def.sel){+      const on=!def.dep||st[def.dep]==='on';+      return '<div class="sub'+(on?'':' sdis')+'" id="row-'+def.k+'"><span class="sdesc">'+esc(def.d)+'</span>'+        +'<select data-sel="'+def.k+'" aria-label="'+esc(def.l)+'">'+def.sel.map(x=>'<option'+(st[def.k]===x?' selected':'')+'>'+x+'</option>').join('')+'</select></div>';+    }+    const on=st[def.k]==='on';+    return '<div class="srow"><div class="stxt"><div class="slbl">'+esc(def.l)+'</div><div class="sdesc">'+esc(def.d)+'</div></div>'+      +'<div class="tgl'+(on?' is-on':'')+'" role="switch" tabindex="0" aria-checked="'+on+'" aria-label="'+esc(def.l)+'" data-tgl="'+def.k+'"></div></div>';+  }).join('');+}+async function setSetting(key,value){+  const j=await act({action:'set-setting',key,value});+  if(j&&j.ok&&j.settings&&SNAP&&SNAP.bridge){SNAP.bridge.settings=j.settings;renderDlg();}+}+document.addEventListener('click',ev=>{+  if(ev.target.closest('#gear')){renderDlg();_q('#dlgWrap').classList.remove('hidden');return;}+  if(ev.target.closest('#dlgClose')||ev.target.id==='dlgBg'){_q('#dlgWrap').classList.add('hidden');return;}+  const t=ev.target.closest('[data-tgl]');+  if(t){setSetting(t.dataset.tgl,t.classList.contains('is-on')?'off':'on');return;}+});+document.addEventListener('input',ev=>{+  const r=ev.target.closest('[data-slider]');+  if(r){_q('#val-'+r.dataset.slider).textContent=(Number(r.value)/1000).toFixed(1)+'s';}+});+document.addEventListener('change',ev=>{+  const r=ev.target.closest('[data-slider]');+  if(r){setSetting(r.dataset.slider,Number(r.value));return;}+  const sl=ev.target.closest('[data-sel]');+  if(sl){setSetting(sl.dataset.sel,sl.value);}+}); (function(){   const b=_q('#brand'),pop=_q('#pop');let show=null,hide=null;   function place(){const r=b.getBoundingClientRect();pop.style.left=Math.max(8,r.left)+'px';pop.style.top=(r.bottom+8)+'px';}@@ -8580,6 +8681,25 @@ const server = http.createServer(async (req, res) => {           try { await tab.page.close(); } catch (e) {}           return done({ ok: true, closed: tid });         }+        if (act === 'set-setting') {+          // v1.9.259: the dashboard settings dialog. Whitelisted keys only; values validated.+          const ALLOWED = {+            aumidIcons: ['on', 'off'], jumpLists: ['on', 'off'], fgWatch: ['on', 'off'],+            fgCaption: ['on', 'off'], overlayBadges: ['on', 'off'], autoFlash: ['on', 'off'],+            openFlash: ['on', 'off'], sleepRecovery: ['on', 'off'], agentActivity: ['on', 'off'],+            reportIssues: ['on', 'off'], autoLogin: ['on', 'off'], minimalTouch: ['on', 'off'],+            taskbarGrouping: ['split', 'grouped'],+          };+          const key = String(body.key || ''); let val = body.value;+          if (key === 'fgCaptionMs') {+            val = Math.max(500, Math.min(5000, Number(val) || 2000));+          } else if (!ALLOWED[key] || !ALLOWED[key].includes(val)) {+            return done({ ok: false, error: 'unknown setting or value' });+          }+          savePupSettings({ [key]: val });+          console.log(`[settings] "${key}" -> ${val} (dashboard dialog)`);+          return done({ ok: true, settings: pupSettings() });+        }         if (act === 'fg' && sid) {           // v1.9.243 (John: "add a 'bring to fg' and 'send to bg' button to each tab/window").           // Self-call the REAL raise verb so every safety behaviour applies unchanged (caption with@@ -12463,7 +12583,7 @@ server.listen(PORT, BIND_HOST, async () => {   // exactly what the user wants after a sleep. (Hung-Chrome teardown, which DOES kill, stays gated.)   let _lastRecoveryTick = Date.now();   setInterval(async () => {-    if (process.platform !== 'win32') return;+    if (process.platform !== 'win32' || pupSettings().sleepRecovery !== 'on') return;     const now = Date.now();     const gap = now - _lastRecoveryTick;     _lastRecoveryTick = now;@@ -12509,7 +12629,7 @@ server.listen(PORT, BIND_HOST, async () => {   // disclaimer caption (2s), mark it foreground (the IRON LAW: whatever raised it, it stays up),   // disarm. NEVER a knockback - that variant shipped as 1.9.252 and was rightly killed same day.   setInterval(async () => {-    if (process.platform !== 'win32') return;+    if (process.platform !== 'win32' || pupSettings().fgWatch !== 'on') return;     try {       const now = Date.now();       const watched = [...sessions].filter(([, s2]) => s2._fgWatchUntil && s2._fgWatchUntil > now && !s2._foreground && !s2._lostBrowser);@@ -12540,6 +12660,10 @@ server.listen(PORT, BIND_HOST, async () => {     try {       for (const [sid2, s2] of [...sessions]) {         if (s2._lostBrowser || !(s2.tabs || []).length) continue;+        if (pupSettings().aumidIcons === 'on' && !s2._curAppId && Date.now() - (s2._stampTriedAt || 0) > 45000) {+          s2._stampTriedAt = Date.now(); stampPupIdentity(sid2, { once: true }).catch(() => {}); continue;+        }+        if (pupSettings().overlayBadges !== 'on') continue;         const bare = !s2._appOverlay;         const stuckGeneric = s2._appOverlay && s2._overlayIsGeneric;         if (!bare && !stuckGeneric) continue;