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.
← Commit history
v2.0.296: Settings dialog rebuilt to the Adom settings-layout standard (adom-ui-design 1.7.1): dismissible modal from the header cog (Escape, X, click-outside), 25% dim never blur, radius element never scrolls, house scrollbar, category sidebar with jump-to + scroll-spy over ONE continuous pane, sections > cards (3px teal accent, live status, full-sentence mechanism prose) > rows (breadcrumb names with teal last part, full key in title, Default on every row), house toggles (role=switch), dependent settings indented under a spine and dimmed never hidden, choice cards with one REAL screenshot per option for taskbar activity / grouping / icon style, two-click auto-disarming Restore defaults (new dashboard action), local-first optimistic saves that keep scroll position. Zero em-dashes.
4 files changed
+274−80
adom-bridge-puppeteer-manifest.json+5−5@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "2.0.295",- "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.295/adom-bridge-puppeteer-v2.0.295.zip",- "sha256": "92b032051464cbf1a6b1e36c265a67484a3fd40be9dae973452e90451ab21cc8",- "size": 1085173,+ "version": "2.0.296",+ "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.296/adom-bridge-puppeteer-v2.0.296.zip",+ "sha256": "78f0303ca807e76d9a8d5922d99f80f2b676936bb3ae6b20120bff41f2b7d4b8",+ "size": 1091700, "verbPrefixes": [ "browser_", "desktop_recorder_",@@ -12,5 +12,5 @@ ], "healthEndpoint": "/status", "statusVerb": "browser_readiness",- "released_at": "2026-08-23T13:03:53.972542Z"+ "released_at": "2026-08-23T13:36:45.151446Z" }
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-2.0.295+2.0.296
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "2.0.295",+ "version": "2.0.296", "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+267−73@@ -12335,14 +12335,98 @@ 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)}-/* v2.0.266: z-index. A position:sticky header with z-index:auto does NOT create a stacking context,- so later siblings that make their own (every .mic is position:relative) paint straight over it —- the sample tiles were scrolling ON TOP of the title and the X button. */-.dlghead{position:sticky;top:0;z-index:5;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}+/* ===== Settings modal (adom-ui-design / settings-layout, v2.0.296) =====+ Tokens first: a scheme swap must change nothing but these. */+#dlgWrap{position:fixed;inset:0;z-index:99998;--surface:#161b22;--elevated:#1c2128;--overlay:#21262d;--border:#30363d;--text:#e6edf3;--text-body:#c9d1d9;--text-2:#8b949e;--accent:#00b8b1;--on-accent:#05221f;--red:#f85149;--green:#3fb950;--brand-heading:"Familjen Grotesk",Satoshi,sans-serif;--brand-body:Satoshi,"Familjen Grotesk",sans-serif}+/* the backdrop law: DIM 25%, never blur (settings often change the very thing behind the modal) */+#dlgBg{position:absolute;inset:0;background:rgba(0,0,0,.25)}+/* the radius element never scrolls: #dlg clips, .settings-body is the scroller */+#dlg{position:absolute;top:48px;left:50%;transform:translateX(-50%);width:min(880px,94vw);max-height:calc(100vh - 96px);display:flex;flex-direction:column;overflow:hidden;padding:0;background:var(--surface);border:1px solid var(--border);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.6);outline:none}+.dlghead{flex:0 0 auto;display:flex;align-items:center;justify-content:space-between;padding:10px 16px;background:var(--surface);border-bottom:1px solid var(--border);font-family:var(--brand-heading);font-weight:600;font-size:15px;color:var(--text)}+.settings-content{display:flex;flex:1 1 auto;min-height:0}+.settings-side{width:13rem;flex-shrink:0;border-right:1px solid var(--border);overflow:auto;padding:.75rem .5rem}+.settings-body{flex:1;min-width:0;overflow:auto;padding:1rem 1.25rem;scroll-behavior:smooth;max-height:70vh}+/* house scrollbar: thin, transparent track, translucent pill, teal on hover */+.settings-body::-webkit-scrollbar,.settings-side::-webkit-scrollbar{width:8px;height:8px}+.settings-body::-webkit-scrollbar-track,.settings-side::-webkit-scrollbar-track{background:transparent;margin:12px 0}+.settings-body::-webkit-scrollbar-thumb,.settings-side::-webkit-scrollbar-thumb{background:rgba(139,148,158,.35);border-radius:8px;border:2px solid transparent;background-clip:padding-box}+.settings-body::-webkit-scrollbar-thumb:hover,.settings-side::-webkit-scrollbar-thumb:hover{background:var(--accent);background-clip:padding-box}+/* sidebar: every entry a button; parents expanded; indentation is the hierarchy cue */+.side-sec{width:100%;padding:.3rem .5rem;border-radius:6px;display:flex;align-items:center;gap:.25rem;background:none;border:none;color:var(--text);font-family:var(--brand-body);font-size:.8125rem;text-align:left;cursor:pointer;user-select:none}+.side-sub{width:100%;padding:.25rem .5rem .25rem 1.5rem;border-radius:6px;display:flex;align-items:center;background:none;border:none;color:var(--text-body);font-family:var(--brand-body);font-size:.75rem;text-align:left;cursor:pointer;user-select:none}+.side-sec:hover,.side-sub:hover{color:var(--accent);background:rgba(255,255,255,.06)}+.side-sec.selected,.side-sub.selected{color:var(--accent);font-weight:700}+.side-sec:focus-visible,.side-sub:focus-visible{outline:2px solid var(--accent);outline-offset:1px}+/* content hierarchy: section (h2) > card (h3) > row */+.sec-title{height:2rem;display:flex;align-items:center;margin:.25rem 0 .5rem;font-family:var(--brand-heading);font-size:1.6825rem;font-weight:800;color:var(--accent);scroll-margin-top:.75rem}+.settings-card{background:var(--elevated);border:1px solid var(--border);border-left:3px solid color-mix(in srgb,var(--accent) 45%,transparent);border-radius:10px;padding:.9rem 1.1rem 1rem;margin-bottom:1rem;scroll-margin-top:.75rem}+.card-title{height:2rem;display:flex;align-items:center;gap:10px;margin:0;font-family:var(--brand-heading);font-size:1.3125rem;font-weight:600;color:var(--accent)}+.card-status{font-family:var(--brand-body);font-size:11px;color:var(--text-2);font-weight:400}+.card-status.ok{color:var(--green)}+.card-desc{margin:.2rem 0 .5rem;font-family:var(--brand-body);font-size:.8125rem;line-height:1.55;color:var(--text-2);max-width:60ch}+.card-desc code{font-size:11px;background:rgba(110,118,129,.2);padding:1px 4px;border-radius:4px}+.divider{height:1px;background:var(--border);margin:.55rem 0}+.set-row{padding:.5rem 0;display:flex;flex-direction:column;gap:.25rem}+.row-head{display:flex;align-items:center;justify-content:space-between;gap:12px}+.row-name{margin:.125rem 0;font-family:var(--brand-heading);font-size:.9375rem;font-weight:600;color:var(--text)}+.row-name .title{color:#8fd3d4}+.row-desc{margin:0;font-family:var(--brand-body);font-size:.8125rem;line-height:1.55;color:var(--text-2);max-width:60ch}+.default-hint{font-family:var(--brand-body);font-size:.75rem;color:var(--text-2)}+.row-why{font-family:var(--brand-body);font-size:.75rem;color:var(--text-2);font-style:italic}+/* dependent settings: indented under the parent with a spine, dimmed (never hidden) when the parent is off */+.setting-children{margin-left:.75rem;padding-left:1rem;border-left:2px solid var(--border)}+.setting-children[data-enabled='false']{opacity:.55}+.setting-children[data-enabled='false'] .row-name{color:var(--text-2)}+.setting-children[data-enabled='false'] .row-name .title{color:var(--text-2)}+/* screenshots of the thing the setting changes: real captures only */+.setting-shot{margin:0 0 .5rem}+.setting-shot img{display:block;max-width:min(100%,520px);height:auto;border:1px solid var(--border);border-radius:8px;background:var(--surface)}+.setting-shot figcaption{margin-top:.35rem;font-family:var(--brand-body);font-size:.75rem;color:var(--text-2)}+.shot-pending{font-family:var(--brand-body);font-size:.75rem;color:var(--text-2);font-style:italic}+/* the house toggle (compact modal size 32x18), role=switch, white thumb in both states */+.toggle{background:none;border:none;padding:2px;cursor:pointer;flex-shrink:0;transition:transform .06s ease;border-radius:10px}+.toggle-track{display:block;width:32px;height:18px;background:var(--overlay);border:1px solid var(--border);border-radius:9px;position:relative;transition:background .15s ease,border-color .15s ease}+.toggle:hover .toggle-track{border-color:var(--accent)}+.toggle:active{transform:translateY(1px) scale(.97)}+.toggle.is-on .toggle-track{background:var(--accent);border-color:var(--accent)}+.toggle-thumb{position:absolute;top:2px;left:2px;width:12px;height:12px;background:#fff;border-radius:50%;transition:transform .18s ease,width .12s ease}+.toggle.is-on .toggle-thumb{transform:translateX(14px)}+.toggle:active .toggle-thumb{width:16px}+.toggle.is-on:active .toggle-thumb{transform:translateX(10px)}+.toggle:focus-visible{outline:2px solid var(--accent);outline-offset:2px}+.toggle[disabled]{cursor:not-allowed}+/* enum controls */+.set-row select{background:var(--overlay);color:var(--text-body);border:1px solid var(--border);border-radius:6px;padding:5px 8px;font-family:var(--brand-body);font-size:.875rem}+.set-row select:hover{border-color:var(--accent)}+.set-row select:focus-visible{outline:2px solid var(--accent);outline-offset:1px}+/* alternatives: one choice card per option, each with its own REAL screenshot, teal ring = selected */+.choice-set{border:none;margin:.25rem 0 .5rem;padding:0;display:flex;flex-wrap:wrap;gap:.75rem}+.choice{position:relative;flex:1 1 200px;display:flex;flex-direction:column;gap:.35rem;padding:.6rem;cursor:pointer;background:var(--surface);border:1px solid var(--border);border-radius:8px;transition:border-color .15s ease,background .15s ease,transform .06s ease}+.choice input{position:absolute;opacity:0;pointer-events:none}+.choice img{display:block;width:100%;height:auto;border-radius:4px;background:#0d1117}+.choice-name{font-family:var(--brand-heading);font-size:.875rem;font-weight:600;color:var(--text)}+.choice-desc{font-family:var(--brand-body);font-size:.75rem;color:var(--text-2);line-height:1.5}+.choice:hover{border-color:var(--accent);background:var(--elevated)}+.choice:active{transform:translateY(1px) scale(.995)}+.choice[data-selected='true']{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}+.choice:focus-within{outline:2px solid var(--accent);outline-offset:2px}+.choice-set[data-enabled='false']{opacity:.55;pointer-events:none}+/* sliders: custom track so the thumb reaches both ends */+.set-row .slider-line{display:flex;align-items:center;gap:10px}+.set-row input[type=range]{flex:1 1 auto;-webkit-appearance:none;appearance:none;height:18px;background:transparent;margin:0}+.set-row input[type=range]::-webkit-slider-runnable-track{height:6px;border-radius:3px;background:var(--overlay);border:1px solid var(--border)}+.set-row input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:16px;height:16px;border-radius:50%;background:var(--accent);margin-top:-6px;box-shadow:0 0 0 1px #0d1117}+.set-row input[type=range]:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:4px}+.sval{font-family:ui-monospace,monospace;font-size:.8125rem;color:var(--text-body);min-width:3.5em;text-align:right}+/* footer: note + two-click Restore defaults (auto-disarms after 4s) */+.settings-footer{margin-top:1.5rem;padding-top:1rem;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:.5rem}+.footer-note{margin:0;font-family:var(--brand-body);font-size:.75rem;color:var(--text-2)}+.restore-btn{width:fit-content;display:inline-flex;align-items:center;gap:.4rem;padding:.4rem .85rem;background:transparent;border:1px solid var(--border);border-radius:.375rem;color:var(--text-body);font-family:var(--brand-heading);font-size:.8125rem;font-weight:600;cursor:pointer;transition:background .12s ease,transform .06s ease}+.restore-btn:hover{background:var(--elevated)}+.restore-btn:active{transform:translateY(1px)}+.restore-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}+.restore-btn.armed{color:var(--red);border-color:rgba(248,81,73,.5);background:rgba(248,81,73,.08)}+#dlgBody{padding:0} .ibadge{display:inline-flex;align-items:center;justify-content:center;width:15px;height:15px;border-radius:50%;border:1px solid #8b949e;color:#8b949e;font-size:10px;font-style:italic;cursor:help;vertical-align:2px} .ibadge:hover{border-color:#00b8b1;color:#00b8b1} .sexample{display:block;max-width:100%;margin-top:8px;border:1px solid #30363d;border-radius:8px}@@ -12385,10 +12469,7 @@ select,input{background:#111b23;color:#e6edf3;border:1px solid #23303c;border-ra .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}+/* .tgl retired in v2.0.296: the settings dialog uses the house .toggle (settings-layout skill) */ .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;-webkit-appearance:none;appearance:none;height:18px;background:transparent;margin:0}.sub input[type=range]::-webkit-slider-runnable-track{height:6px;border-radius:3px;background:#2a3038}.sub input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:16px;height:16px;border-radius:50%;background:#00b8b1;margin-top:-5px;box-shadow:0 0 0 1px #0d1117}@@ -12470,7 +12551,7 @@ footer{color:#5c6c7a;font-size:11px;padding:8px 18px 22px} <span class="smeta"><b id="cW">0</b> windows · <b id="cT">0</b> tabs · <b id="cB">0</b> icons · <span id="cAY" title="windows surfing on the shared adom-you profile / total work windows. More adom-you = a richer shared login store = pup auto-fills more sites for you over time.">?</span> adom-you · <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> · <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" id="gear" title="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="regen-overlays" data-tip="Regenerate ALL overlay icons: re-scan every open window and tab and re-run the from-scratch overlay algorithm (re-resolve each window handle, re-pick the favicon, recompose + repaint the taskbar badge) and refresh these dashboard icons to match. A clean sweep to verify correctness and surface any drift." title="regenerate all overlay icons (clean sweep)" aria-label="regenerate overlays"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg></button> <button class="ibtn" data-act="flash-all" title="flash every pup taskbar button (the Windows taskbar flash, accent-colored on your theme)" 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 taskbar flash" aria-label="clear taskbar 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>@@ -12485,9 +12566,12 @@ footer{color:#5c6c7a;font-size:11px;padding:8px 18px 22px} <div id="toasts"></div> <div id="dlgWrap" class="hidden"> <div id="dlgBg"></div>- <div id="dlg" role="dialog" aria-label="Settings">- <div class="dlghead">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 id="dlg" role="dialog" aria-modal="true" aria-label="pup Settings" tabindex="-1">+ <div class="dlghead">pup Settings<button class="ibtn" id="dlgClose" title="Close settings" aria-label="Close settings"><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 class="settings-content">+ <nav class="settings-side" id="dlgSide" aria-label="Settings sections"></nav>+ <div class="settings-body" id="dlgScroller"><div id="dlgBody"></div></div>+ </div> </div> </div> <div id="taskbar"></div>@@ -12621,7 +12705,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 the taskbar button of','unflash':'taskbar flash cleared on','flash-all':'flashed every pup taskbar button','unflash-all':'cleared every taskbar flash','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','test-caption':'sample caption shown on your screen','regen-overlays':'regenerated all overlay icons','import-scan':'scanned your browsers for logins','import-run':'import'};+const ACTMSG={'flash':'flashed the taskbar button of','unflash':'taskbar flash cleared on','flash-all':'flashed every pup taskbar button','unflash-all':'cleared every taskbar flash','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','test-caption':'sample caption shown on your screen','restore-defaults':'every setting restored to its default','regen-overlays':'regenerated all overlay icons','import-scan':'scanned your browsers for logins','import-run':'import'}; async function act(p){ try{ const r=await fetch('/dashboard/action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(p)});@@ -12673,10 +12757,10 @@ const SETTINGS_DEF=[ // part of the same decision — the identity picker's own examples are 'tiles + overlay' vs 'tiles', // so having the overlay toggle sit fifteen rows further down was a genuine discoverability bug. {k:'overlayBadges',l:'Favicon overlay badges',d:'Paint each pup taskbar icon with the favicon of its active tab plus the Adom mark.'},- {k:'overlaySize',l:'Badge size',d:'Percent of full size for the badge. Smaller keeps it clear of the tile\u2019s text.',sel:['50','66','75','100'],selLabels:{'50':'50% \u2014 smallest, maximum text clearance','66':'66%','75':'75% \u2014 balanced (default)','100':'100% \u2014 full size'},dep:'overlayBadges'},- {k:'taskbarGrouping',l:'AUMID grouping',d:'Only applies when AUMID icons are on. split = one taskbar button per window. grouped = windows grouped BY CATEGORY when icon-style is "category" (up to 5 buttons: signed-in wiki, public wiki, open web, Adom app, plain — e.g. all your signed-in-wiki windows collapse under the one padlock button); with icon-style "standard" grouped collapses everything under a single Adom Pup button.',sel:['split','grouped'],depMode:'aumid',imgBy:{split:'groupsplit',grouped:'groupgrouped'}},+ {k:'overlaySize',l:'Badge size',d:'Percent of full size for the badge. Smaller keeps it clear of the tile\u2019s text.',sel:['50','66','75','100'],selLabels:{'50':'50%, smallest, maximum text clearance','66':'66%','75':'75%, balanced','100':'100%, full size'},dep:'overlayBadges'},+ {k:'taskbarGrouping',l:'AUMID grouping',d:'Only applies when AUMID icons are on. split = one taskbar button per window. grouped = windows grouped BY CATEGORY when icon-style is "category" (up to 5 buttons: signed-in wiki, public wiki, open web, Adom app, plain: for example all your signed-in-wiki windows collapse under the one padlock button); with icon-style "standard" grouped collapses everything under a single Adom Pup button.',sel:['split','grouped'],depMode:'aumid',imgBy:{split:'groupsplit',grouped:'groupgrouped'}}, {k:'aumidIconStyle',l:'AUMID icon style',d:'Only applies when AUMID icons are on: standard = one pup tile for every window; category = the custom set (wiki signed-out, wiki signed-in padlock, world icon for the open web, adom app icon, mixed).',sel:['standard','category'],depMode:'aumid',imgBy:{standard:'iconstylestd',category:'iconstylecat'}},- {k:'jumpLists',l:'Jump lists',d:'The menu Windows shows when you right-click a pup taskbar icon (real screenshot below, captured on this machine). Requires AUMID icons on. Off by default. Quirk: the FIRST right-click can blip the menu closed — right-click again ~1s later and it holds (a Windows shell timing race, not a pup bug).',img:'jumplist'},+ {k:'jumpLists',l:'Jump lists',d:'The menu Windows shows when you right-click a pup taskbar icon (real screenshot below, captured on this machine). Requires AUMID icons on. Quirk: the FIRST right-click can blip the menu closed; right-click again about a second later and it holds (a Windows shell timing race, not a pup bug).',img:'jumplist'}, {k:'jlViewTasks',l:'',d:'Wiki view tasks: Switch to logged-in view / Switch to public view on every wiki window.',dep:'jumpLists',subtgl:1,img:'jlviewtasks'}, {k:'jlCloseAll',l:'',d:'The Close all pup windows task on every pup icon.',dep:'jumpLists',subtgl:1,img:'jlcloseall'}, {k:'jlDashboard',l:'',d:'The Open pup dashboard task on every pup icon: one click opens the live window/tab/thread map.',dep:'jumpLists',subtgl:1},@@ -12700,7 +12784,7 @@ const SETTINGS_DEF=[ {k:'titleTag',l:'ai-thread tag in the window title',d:'Append "(ai-thread: <name>)" to each window title so YOU can tell windows apart from the title bar. Off by default because it can look like the page mangled its own title. Purely cosmetic now: pup identifies its own windows by a Win32 property tag it writes at birth, so it never needs the title.'}, {k:'titlePrefix',l:'Thread name prefixed to the title',d:'Prepend the owning thread (and wiki logged-in/out state) to the start of each window title. Off by default.'}, {k:'ownerChip',l:'Floating owner chip in each window',d:'Show a tiny non-interactive chip in the bottom-left of each pup window naming the AI thread that owns it and its profile. It cannot be clicked through to (pointer-events off) and lives in an isolated layer so it never affects the page. Turn off if you do not want any overlay in your windows.'},- {k:'notMineCaption',l:'"Not foregrounded by Adom" caption',d:'When a window comes to the front on its own (a page’s JavaScript, a focus change, Chrome — e.g. a PDF viewer self-activating), briefly caption that Adom did NOT do it. Stays SILENT when YOU raised the window (detected via recent taskbar/keyboard input). This is the single control for self-foreground captions, including the 20s right after a window opens. Off by default.'},+ {k:'notMineCaption',l:'"Not foregrounded by Adom" caption',d:'When a window comes to the front on its own (a page’s JavaScript, a focus change, Chrome itself, for example a PDF viewer self-activating), briefly caption that Adom did NOT do it. Stays SILENT when YOU raised the window (detected via recent taskbar/keyboard input). This is the single control for self-foreground captions, including the 20s right after a window opens. Off by default.'}, {k:'minimalTouch',l:'Minimal-touch mode',d:'The safety mode from the Chrome crash hunt. ON means pup keeps its hands off your windows: it only opens them and drives the pages inside, and skips the extra Windows machinery (identity stamps, window sweeps, shell churn) that was suspected of freezing Chrome. The toggles above are the real per-feature controls now and they win over this switch. Rule of thumb: leave this ON; if pup ever gets crashy, make sure it is ON and turn features back on one at a time to find the culprit.'}, ]; // Import-logins picker (the adom-you seed). Discovery runs today via pup_import_scan; the@@ -12744,58 +12828,140 @@ async function runImport(){ if(el)el.textContent=j.message||j.error||'Done.'; toast(j.ok?('Imported '+((j.added||0)+(j.updated||0))+' logins into the adom-you vault'):(j.message||j.error||'Import failed'),j.ok?'success':'error'); }+// ===== Settings renderer (adom-ui-design / settings-layout, v2.0.296) =====+// One continuously scrolling pane: sections (h2) > cards (h3) > rows, a sidebar that jumps within it+// and scroll-spies, every row a breadcrumb name + explanation + (real) screenshot + control + default.+const SET_NAMES={taskbarMode:'Identity mode',overlayBadges:'Favicon overlay badges',overlaySize:'Badge size',taskbarGrouping:'Grouping',aumidIconStyle:'Icon style',jumpLists:'Jump lists',jlViewTasks:'Wiki view tasks',jlCloseAll:'Close all windows task',jlDashboard:'Open dashboard task',jlAnnotate:'Annotate this page task',annCaptionBar:'Caption bar on captures',toolbarExtension:'Adom toolbar in Chrome',tbSnipHotkey:'Snip hotkey',tbFloatyPencil:'Corner pencil',floatyHitPx:'Pencil hit area',taskbarActivity:'Activity signal',fgCaption:'Caption on AI foreground',fgCaptionMs:'Minimum caption time',notMineCaption:'Not-by-Adom caption',sleepRecovery:'Sleep and crash recovery',autoLogin:'Auto-fill saved logins',captureLogins:'Capture logins to the vault',reportIssues:'Issue-report hints',titleTag:'ai-thread tag in the title',titlePrefix:'Thread name prefixed to the title',ownerChip:'Floating owner chip',minimalTouch:'Minimal-touch mode'};+const SET_DEFAULTS={taskbarMode:'Tiles',overlayBadges:'On',overlaySize:'75%, balanced',taskbarGrouping:'Split',aumidIconStyle:'Category',jumpLists:'On',jlViewTasks:'On',jlCloseAll:'On',jlDashboard:'On',jlAnnotate:'On',annCaptionBar:'On',toolbarExtension:'On',tbSnipHotkey:'On',tbFloatyPencil:'Off',floatyHitPx:'24px',taskbarActivity:'Progress bar',fgCaption:'On',fgCaptionMs:'3.5s',notMineCaption:'Off',sleepRecovery:'On',autoLogin:'On',captureLogins:'On',reportIssues:'On',titleTag:'Off',titlePrefix:'Off',ownerChip:'On',minimalTouch:'On'};+// choice cards: the option name + the one-line tradeoff + the REAL screenshot per option+const SET_CHOICES={+ taskbarActivity:[{v:'progress',n:'Progress bar',d:'Fills over the operation’s usual duration, so you can see how far along it is.',img:'tbactprogress',alt:'The pup taskbar button with a blue progress bar filling underneath it'},+ {v:'flash',n:'Orange flash',d:'Highlights the button until you look at it. Tells you something happened, not how long is left.',img:'tbactflash',alt:'The same taskbar button with the attention highlight underneath it'},+ {v:'off',n:'Off',d:'The taskbar button never changes.',img:'tbactoff',alt:'The same taskbar button with no highlight of any kind'}],+ taskbarGrouping:[{v:'split',n:'Split',d:'One taskbar button per window.',img:'groupsplit',alt:'Five separate pup taskbar buttons, one per window'},+ {v:'grouped',n:'Grouped',d:'Windows of the same category share one button.',img:'groupgrouped',alt:'The same five windows collapsed under category buttons'}],+ aumidIconStyle:[{v:'standard',n:'Standard',d:'One pup tile for every window.',img:'iconstylestd',alt:'Pup taskbar buttons all wearing the same pup tile'},+ {v:'category',n:'Category',d:'A tile per kind: signed-in wiki, public wiki, open web, Adom app, mixed.',img:'iconstylecat',alt:'Pup taskbar buttons each wearing the icon of its category'}],+};+const SET_SECTIONS=[+ {id:'s-identity',t:'Taskbar identity',cards:[+ {id:'c-mode',t:'Identity mode',status:st=>st.taskbarMode,desc:'How pup brands your Windows taskbar. Tiles gives every AI thread one button whose icon is the thread name on the Adom teal tile, so the taskbar reads as a list of your threads. AUMID gives every window its own button with a pup or category icon. Plain leaves identity alone and windows stack under the stock Chrome for Testing button. The example tiles are drawn live by the same code that draws the real icons, so they cannot drift from what Windows shows.',rows:['taskbarMode']},+ {id:'c-badges',t:'Overlay badges',status:st=>st.overlayBadges==='on'?'on':'off',desc:'The favicon of the active tab, composited into the corner of the taskbar button so you can tell at a glance which site a window is on. Works in tiles and AUMID modes.',rows:['overlayBadges','overlaySize']},+ {id:'c-aumid',t:'AUMID options',status:st=>st.taskbarMode==='aumid'?'active':'inert in '+st.taskbarMode+' mode',desc:'These two only apply in AUMID mode, where each window carries its own button.',rows:['taskbarGrouping','aumidIconStyle'],depMode:'aumid'},+ ]},+ {id:'s-jumplists',t:'Jump lists',cards:[+ {id:'c-jl',t:'Right-click menu',status:st=>st.jumpLists==='on'?'on':'off',desc:'The menu Windows shows when you right-click a pup taskbar button. pup registers the tasks below through the Windows jump-list API, so they appear exactly where every other app puts them. Requires an identity mode other than plain.',rows:['jumpLists','jlViewTasks','jlCloseAll','jlDashboard','jlAnnotate']},+ ]},+ {id:'s-capture',t:'Annotate and capture',cards:[+ {id:'c-capbar',t:'Captures',status:st=>st.annCaptionBar==='on'?'caption bar on':'caption bar off',desc:'When you draw on a page and finish, pup captures the marked-up region as a PNG and copies it to your clipboard.',rows:['annCaptionBar']},+ {id:'c-toolbar',t:'Adom toolbar in Chrome',status:st=>st.toolbarExtension==='on'?'on':'off',desc:'A small family of extension buttons that ride the Chrome for Testing launch: the pup menu, the annotate pen, and your adom-you identity tile. They are staged on every launch, so they need no setup.',rows:['toolbarExtension','tbSnipHotkey','tbFloatyPencil','floatyHitPx']},+ ]},+ {id:'s-activity',t:'Activity and foreground',cards:[+ {id:'c-activity',t:'Taskbar activity signal',status:st=>({progress:'progress bar',flash:'orange flash',off:'off'})[st.taskbarActivity]||st.taskbarActivity,desc:'What a window’s taskbar button does while pup is working on it. pup keeps a running average of how long each operation takes (applying the window icon, taking a screenshot) and fills the bar over that estimate, capping just short of done until the work actually finishes. Windows only.',rows:['taskbarActivity']},+ {id:'c-captions',t:'Foreground captions',status:st=>st.fgCaption==='on'?'on':'off',desc:'pup opens every window in the background and never brings one to your screen without a reason. When an AI does raise a window, it must state why, and that reason is shown as an on-screen caption BEFORE the window comes up, naming the thread that asked. Caption time scales with the amount of text: the slider sets the minimum, and longer reasons extend at reading speed up to 12 seconds.',rows:['fgCaption','fgCaptionMs','notMineCaption']},+ ]},+ {id:'s-logins',t:'Logins',cards:[+ {id:'c-vault',t:'Credential vault',status:st=>st.captureLogins==='on'?'capturing':'not capturing',desc:'pup runs its own password manager backed by the OS keychain, so an AI can sign into sites in the background without Chrome’s foreground autofill dropdown. Passwords are encrypted at rest and never logged or echoed.',rows:['autoLogin','captureLogins']},+ {id:'c-import',t:'Import logins',html:()=>importSectionHtml()},+ ]},+ {id:'s-inpage',t:'In-page identity',cards:[+ {id:'c-labels',t:'Window labels',status:st=>st.ownerChip==='on'?'owner chip on':'owner chip off',desc:'Ways to tell which AI thread owns a window from inside it. pup identifies its own windows by a Win32 property it writes at birth, so none of these are needed for bookkeeping; they exist only for you.',rows:['ownerChip','titleTag','titlePrefix']},+ ]},+ {id:'s-safety',t:'Recovery and safety',cards:[+ {id:'c-safety',t:'Recovery and safety',status:st=>st.sleepRecovery==='on'?'recovery on':'recovery off',desc:'What pup does when things go wrong, and how much it touches your windows beyond driving the pages inside them.',rows:['sleepRecovery','reportIssues','minimalTouch']},+ ]},+];+const SET_PREFIX={taskbarMode:'Taskbar: ',overlayBadges:'Taskbar: ',overlaySize:'Overlay badges: ',taskbarGrouping:'AUMID: ',aumidIconStyle:'AUMID: ',jumpLists:'Taskbar: ',jlViewTasks:'Jump lists: ',jlCloseAll:'Jump lists: ',jlDashboard:'Jump lists: ',jlAnnotate:'Jump lists: ',annCaptionBar:'Captures: ',toolbarExtension:'Chrome: ',tbSnipHotkey:'Toolbar: ',tbFloatyPencil:'Toolbar: ',floatyHitPx:'Corner pencil: ',taskbarActivity:'Taskbar: ',fgCaption:'Foreground: ',fgCaptionMs:'Caption: ',notMineCaption:'Foreground: ',sleepRecovery:'Recovery: ',autoLogin:'Vault: ',captureLogins:'Vault: ',reportIssues:'Safety: ',titleTag:'Title: ',titlePrefix:'Title: ',ownerChip:'In page: ',minimalTouch:'Safety: '};+const SET_DEFS=Object.fromEntries(SETTINGS_DEF.map(d=>[d.k,d]));+let _dlgSelected=null;+function _rowName(k){return '<h4 class="row-name" title="'+esc(k)+'"><span class="prefix">'+esc(SET_PREFIX[k]||'')+'</span><span class="title">'+esc(SET_NAMES[k]||k)+'</span></h4>';}+function _toggle(k,on,dis){return '<button class="toggle'+(on?' is-on':'')+'" role="switch" aria-checked="'+on+'" aria-label="'+esc(SET_NAMES[k]||k)+'" data-tgl="'+k+'"'+(dis?' disabled':'')+'><span class="toggle-track"><span class="toggle-thumb"></span></span></button>';}+function _shot(name,alt,cap){return name?'<figure class="setting-shot"><img src="/dashboard/example?n='+name+'&v='+PAGE_VERSION+'" alt="'+esc(alt||'')+'" loading="lazy" data-fallback="hide">'+(cap?'<figcaption>'+esc(cap)+'</figcaption>':'')+'</figure>':'';}+function _dflt(k){return '<span class="default-hint">Default: '+esc(SET_DEFAULTS[k]||'')+'</span>';}+function renderRow(k,st,enabled){+ const def=SET_DEFS[k]; if(!def) return '';+ const dis=!enabled;+ if(def.slider){+ const v=Number(st[k])||(def.unit==='px'?24:3500); const unit=def.unit||'s';+ const disp=unit==='px'?v+'px':(v/1000).toFixed(1)+'s';+ return '<div class="set-row" id="row-'+k+'">'+_rowName(k)+'<p class="row-desc">'+esc(def.sliderLabel||'')+'</p>'+ +'<div class="slider-line"><input type="range" min="'+def.slider[0]+'" max="'+def.slider[1]+'" step="'+def.slider[2]+'" value="'+v+'" data-slider="'+k+'" data-unit="'+unit+'" aria-label="'+esc(SET_NAMES[k]||k)+'"'+(dis?' disabled':'')+'>'+ +'<span class="sval" id="val-'+k+'">'+disp+'</span>'+ +(def.test?'<button class="btn sm" data-testcap="1" title="Show a sample caption on your screen now, at the configured duration"'+(dis?' disabled':'')+'>test</button>':'')+'</div>'+_dflt(k)+'</div>';+ }+ if(SET_CHOICES[k]){+ const cur=st[k];+ return '<div class="set-row" id="row-'+k+'">'+_rowName(k)+'<p class="row-desc">'+esc(def.d)+'</p>'+ +'<fieldset class="choice-set" role="radiogroup" aria-label="'+esc(SET_NAMES[k]||k)+'" data-enabled="'+(!dis)+'">'+ +SET_CHOICES[k].map(c=>'<label class="choice" data-selected="'+(cur===c.v)+'"><input type="radio" name="sc-'+k+'" value="'+c.v+'" data-choice="'+k+'"'+(cur===c.v?' checked':'')+(dis?' disabled':'')+'>'+ +'<img src="/dashboard/example?n='+c.img+'&v='+PAGE_VERSION+'" alt="'+esc(c.alt)+'" loading="lazy" data-fallback="hide">'+ +'<span class="choice-name">'+esc(c.n)+'</span><span class="choice-desc">'+esc(c.d)+'</span></label>').join('')+ +'</fieldset>'+_dflt(k)+'</div>';+ }+ if(def.sel){+ const selHtml='<select data-sel="'+k+'" aria-label="'+esc(SET_NAMES[k]||k)+'"'+(dis?' disabled':'')+'>'+def.sel.map(x=>'<option value="'+esc(x)+'"'+(st[k]===x?' selected':'')+'>'+esc((def.selLabels&&def.selLabels[x])||x)+'</option>').join('')+'</select>';+ const threadEx=def.threadex?'<div class="texwrap"><div class="texstrip" id="texstrip"></div><div class="texcap">Your real thread names, drawn exactly as the taskbar draws them. The badge in the corner is the favicon of the active tab.</div></div>':'';+ return '<div class="set-row" id="row-'+k+'"><div class="row-head">'+_rowName(k)+selHtml+'</div><p class="row-desc">'+esc(def.d)+'</p>'+threadEx+_dflt(k)+'</div>';+ }+ const on=st[k]==='on';+ const shot=def.img?_shot(def.img,SET_NAMES[k]+' as it appears on this machine',null):'';+ return '<div class="set-row" id="row-'+k+'"><div class="row-head">'+_rowName(k)+_toggle(k,on,dis)+'</div><p class="row-desc">'+esc(def.d)+'</p>'+shot+_dflt(k)+'</div>';+}+function renderCard(card,st){+ const status=card.status?card.status(st):'';+ const okish=/^(on|active|tiles|aumid|progress bar|orange flash|capturing|recovery on|owner chip on|caption bar on)$/.test(status);+ let body='';+ if(card.html){ body=card.html(); }+ else{+ const keys=card.rows;+ const parentKey=keys[0];+ const parentDef=SET_DEFS[parentKey];+ const cardOn=card.depMode?(st.taskbarMode===card.depMode):true;+ // the first row is the parent; rows that declare dep on it (or on a sibling) are its children+ const children=keys.slice(1).filter(k=>SET_DEFS[k]&&(SET_DEFS[k].dep===parentKey||keys.includes(SET_DEFS[k].dep)));+ const siblings=keys.slice(1).filter(k=>!children.includes(k));+ body+=renderRow(parentKey,st,cardOn);+ if(children.length){+ const pOn=cardOn&&(parentDef&&parentDef.sel?true:st[parentKey]==='on');+ body+='<div class="setting-children" data-enabled="'+pOn+'" title="'+(pOn?'':'Available when '+esc(SET_NAMES[parentKey])+' is on')+'">'+ +(pOn?'':'<div class="row-why">Available when '+esc(SET_NAMES[parentKey])+' is on.</div>')+ +children.map((k,i)=>{+ const d=SET_DEFS[k]; const gp=d.dep&&d.dep!==parentKey?d.dep:null; // grandchild (e.g. pencil hit area under corner pencil)+ const en=pOn&&(!gp||st[gp]==='on');+ return (i?'<div class="divider"></div>':'')+renderRow(k,st,en);+ }).join('')+'</div>';+ }+ siblings.forEach(k=>{ body+='<div class="divider"></div>'+renderRow(k,st,cardOn); });+ }+ return '<section class="settings-card" id="'+card.id+'"><h3 class="card-title">'+esc(card.t)+(status?' <span class="card-status'+(okish?' ok':'')+'">'+esc(status)+'</span>':'')+'</h3>'+ +(card.desc?'<p class="card-desc">'+esc(card.desc)+'</p>':'')+body+'</section>';+} function renderDlg(){ const st=(SNAP&&SNAP.bridge&&SNAP.bridge.settings)||{}; st.taskbarMode=st.aumidIcons!=='on'?'plain':(st.threadTiles==='on'?'tiles':'aumid');- _q('#dlgBody').innerHTML=SETTINGS_DEF.map(def=>{- // v2.0.172 (John: "in that section you should have tons of example taskbar icons like the aumid- // section, so the user understands the difference"): Thread Tiles examples are LIVE-DRAWN by the- // dialog (same teal, same font, same bottom-left anchor + wrap rules as the real .ico renderer),- // so they can never drift from the icons Windows actually shows, and they need no shipped PNGs.- const threadEx=def.threadex?'<div class="texwrap"><div class="texstrip" id="texstrip"></div>'- +'<div class="texcap">Your real thread names, drawn exactly as the taskbar draws them. The badge in the corner is the favicon of the active tab.</div></div>':'';- const _imgName=def.imgBy?def.imgBy[st[def.k]]||def.imgBy[Object.keys(def.imgBy)[0]]:def.img;- const exImg=_imgName?'<img class="sexample" src="/dashboard/example?n='+_imgName+'&v='+PAGE_VERSION+'" alt="example" data-fallback="hide">':'';- if(def.slider){- const on=st[def.dep]==='on';- const v=Number(st[def.k])||3500;- const lbl=def.sliderLabel||'caption duration';- const unit=def.unit||'s';- const disp=unit==='px'?v+'px':(v/1000).toFixed(1)+'s';- return '<div class="sub'+(on?'':' sdis')+'" id="row-'+def.k+'"><span class="sdesc">'+lbl+'</span>'- +'<input type="range" min="'+def.slider[0]+'" max="'+def.slider[1]+'" step="'+def.slider[2]+'" value="'+v+'" data-slider="'+def.k+'" data-unit="'+unit+'" aria-label="'+lbl+'">'- +'<span class="sval" id="val-'+def.k+'">'+disp+'</span>'- +(def.test?'<button class="btn sm" data-testcap="1" title="show a sample caption on your screen right now, at the configured duration">test</button>':'')+'</div>';- }- if(def.subtgl){- const pOn=st[def.dep]==='on';- const on=st[def.k]==='on';- return '<div class="sub'+(pOn?'':' sdis')+'" id="row-'+def.k+'"><div style="flex:1 1 auto;min-width:0"><span class="sdesc">'+esc(def.d)+'</span>'+exImg+'</div>'- +'<div class="tgl'+(on?' is-on':'')+'" role="switch" tabindex="0" aria-checked="'+on+'" aria-label="'+esc(def.d)+'" data-tgl="'+def.k+'"></div></div>';- }- if(def.sel){- const on=def.depMode?(st.taskbarMode===def.depMode):(!def.dep||st[def.dep]==='on');- // v2.0.262: optional selLabels gives a raw stored value a human option label ('50' -> 'Half- // size'). Without it the select showed the literal value, which is fine for mode words like- // "split"/"grouped" but meaningless for a number.- const selHtml='<select data-sel="'+def.k+'" aria-label="'+esc(def.l||def.k)+'">'+def.sel.map(x=>'<option value="'+esc(x)+'"'+(st[def.k]===x?' selected':'')+'>'+esc((def.selLabels&&def.selLabels[x])||x)+'</option>').join('')+'</select>';- // v2.0.179: mainsel = a top-level MODE row (label + info + description + examples + select),- // used by the taskbar-identity ladder; plain sel rows stay compact subs.- if(def.mainsel){- return '<div class="srow"><div class="stxt"><div class="slbl">'+esc(def.l)+(def.info?' <span class="ibadge" data-tip="'+esc(def.info)+'">i</span>':'')+'</div><div class="sdesc">'+esc(def.d)+'</div>'+exImg+threadEx+'</div>'+selHtml+'</div>';- }- return '<div class="sub'+(on?'':' sdis')+'" id="row-'+def.k+'"><div style="flex:1 1 auto;min-width:0"><span class="sdesc">'+esc(def.d)+'</span>'+exImg+'</div>'- +selHtml+'</div>';- }- const on=st[def.k]==='on';- const testRow=def.testflash?'<div class="sub"><span class="sdesc">'+((SNAP&&SNAP.counts&&SNAP.counts.windows)||0)+' pup window(s) open right now; their taskbar buttons will flash. Note: Windows never flashes the button of the window you are LOOKING AT, so if this dashboard is your active window its own button stays unlit.</span>'- +'<button class="btn sm" data-act="flash-all" title="flash every open pup taskbar button so you can see what this looks like">test flash</button>'- +'<button class="btn sm" data-act="unflash-all" title="clear every taskbar flash">clear flash</button></div>':'';- return '<div class="srow"><div class="stxt"><div class="slbl">'+esc(def.l)+(def.info?' <span class="ibadge" data-tip="'+esc(def.info)+'">i</span>':'')+'</div><div class="sdesc">'+esc(def.d)+'</div>'+exImg+threadEx+testRow+'</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('')+importSectionHtml();+ _q('#dlgSide').innerHTML=SET_SECTIONS.map(sec=>'<button class="side-sec'+(_dlgSelected===sec.id?' selected':'')+'" data-jump="'+sec.id+'">'+esc(sec.t)+'</button>'+ +sec.cards.map(c=>'<button class="side-sub'+(_dlgSelected===c.id?' selected':'')+'" data-jump="'+c.id+'">'+esc(c.t)+'</button>').join('')).join('');+ _q('#dlgBody').innerHTML=SET_SECTIONS.map(sec=>'<h2 class="sec-title" id="'+sec.id+'">'+esc(sec.t)+'</h2>'+sec.cards.map(c=>renderCard(c,st)).join('')).join('')+ +'<footer class="settings-footer"><p class="footer-note">Settings are stored on this machine (~/.adom/pup-settings.json) and apply to live windows immediately. Restoring returns every option above to its default; your identity avatar is kept.</p>'+ +'<button class="restore-btn" id="restoreBtn">Restore defaults</button></footer>'; paintThreadExamples(); }+function openDlg(){+ renderDlg(); _q('#dlgWrap').classList.remove('hidden');+ const dlg=_q('#dlg'), sc=_q('#dlgScroller');+ try{dlg.focus();}catch(e){}+ sc.scrollTop=0; requestAnimationFrame(()=>{sc.scrollTop=0; spyDlg();});+}+function closeDlg(){_q('#dlgWrap').classList.add('hidden');}+let _spyRaf=0,_spyMuteUntil=0;+function spyDlg(){+ const pane=_q('#dlgScroller'); if(!pane) return;+ const line=pane.getBoundingClientRect().top+pane.clientHeight*0.33;+ let cur=null;+ for(const h of pane.querySelectorAll('.sec-title,.settings-card')){ if(h.getBoundingClientRect().top<=line) cur=h.id; else break; }+ if(cur&&cur!==_dlgSelected){_dlgSelected=cur; document.querySelectorAll('#dlgSide [data-jump]').forEach(b=>b.classList.toggle('selected',b.dataset.jump===cur));}+}+let _restoreArmed=0; // v2.0.172 THREAD TILE EXAMPLES — draw sample taskbar tiles on canvas using the SAME rules as // renderThreadTileIco on the server (teal #00b8b1, ink #05221f, auto-fit, 2-line wrap on the last // space or the first hyphen, text anchored BOTTOM-LEFT so the top-right favicon badge stays clear).@@ -12966,14 +13132,26 @@ function drawNameTile(ctx,box,nm){ } async function setSetting(key,value){+ // optimistic: the control already reflects the click; the re-render after the save must not+ // jump the scroller (settings-layout: one continuous pane, position preserved across saves).+ const sc=_q('#dlgScroller'); const keepTop=sc?sc.scrollTop:0; const j=await act({action:'set-setting',key,value});- if(j&&j.ok&&j.settings&&SNAP&&SNAP.bridge){SNAP.bridge.settings=j.settings;renderDlg();}+ if(j&&j.ok&&j.settings&&SNAP&&SNAP.bridge){SNAP.bridge.settings=j.settings;renderDlg();if(sc){sc.scrollTop=keepTop;}} } 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;}+ if(ev.target.closest('#gear')){openDlg();return;}+ if(ev.target.closest('#dlgClose')||ev.target.id==='dlgBg'){closeDlg();return;}+ const j=ev.target.closest('[data-jump]');+ if(j){_dlgSelected=j.dataset.jump;_spyMuteUntil=Date.now()+600;document.querySelectorAll('#dlgSide [data-jump]').forEach(b=>b.classList.toggle('selected',b===j));const el=document.getElementById(j.dataset.jump);if(el)el.scrollIntoView({block:'start'});return;}+ if(ev.target.closest('#restoreBtn')){+ const b=_q('#restoreBtn');+ if(!_restoreArmed){_restoreArmed=Date.now();b.classList.add('armed');b.textContent='Click again to confirm';setTimeout(()=>{if(_restoreArmed&&Date.now()-_restoreArmed>=3900){_restoreArmed=0;b.classList.remove('armed');b.textContent='Restore defaults';}},4000);return;}+ _restoreArmed=0;b.classList.remove('armed');b.textContent='Restoring…';+ act({action:'restore-defaults'}).then(j=>{if(j&&j.ok&&j.settings&&SNAP&&SNAP.bridge){SNAP.bridge.settings=j.settings;}renderDlg();});+ return;+ } const t=ev.target.closest('[data-tgl]');- if(t){setSetting(t.dataset.tgl,t.classList.contains('is-on')?'off':'on');return;}+ if(t){if(t.disabled)return;const next=t.classList.contains('is-on')?'off':'on';t.classList.toggle('is-on',next==='on');t.setAttribute('aria-checked',next==='on');setSetting(t.dataset.tgl,next);return;} if(ev.target.closest('[data-testcap]')){act({action:'test-caption'});return;} if(ev.target.closest('#importScanBtn')){runImportScan();return;} if(ev.target.closest('#importRunBtn')){runImport();return;}@@ -12986,7 +13164,11 @@ document.addEventListener('input',ev=>{ const r=ev.target.closest('[data-slider]'); if(r){_q('#val-'+r.dataset.slider).textContent=r.dataset.unit==='px'?r.value+'px':(Number(r.value)/1000).toFixed(1)+'s';} });+document.addEventListener('keydown',ev=>{ if(ev.key==='Escape'&&!_q('#dlgWrap').classList.contains('hidden')){closeDlg();} });+document.addEventListener('scroll',ev=>{ if(ev.target&&ev.target.id==='dlgScroller'){ if(_spyRaf)return; _spyRaf=requestAnimationFrame(()=>{_spyRaf=0; if(Date.now()<_spyMuteUntil)return; spyDlg();}); } },true); document.addEventListener('change',ev=>{+ const c=ev.target.closest('[data-choice]');+ if(c){const set=c.closest('.choice-set');if(set)set.querySelectorAll('.choice').forEach(l=>l.dataset.selected=String(l.contains(c)));setSetting(c.dataset.choice,c.value);return;} const r=ev.target.closest('[data-slider]'); if(r){setSetting(r.dataset.slider,Number(r.value));return;} const sl=ev.target.closest('[data-sel]');@@ -13193,6 +13375,18 @@ const server = http.createServer(async (req, res) => { stats: { profilesRead: st.profilesRead, passwordsOk: st.passwordsOk, passwordsFailed: st.passwordsFailed, cookiesFailed: st.cookiesFailed }, message: `Imported ${merge.added + merge.updated} logins into your adom-you vault (${merge.added} new, ${merge.updated} updated${merge.skipped ? ', ' + merge.skipped + ' skipped' : ''}), tagged by source account. pup can now sign in as you on those sites.` }); }+ if (act === 'restore-defaults') {+ // v2.0.296 (settings-layout skill: the two-click Restore defaults footer). Wipe every+ // stored option so the D block's defaults apply; the user's IDENTITY (avatar + name)+ // is data, not an option, so it survives.+ const keep = {};+ for (const k of ['identityAvatarB64', 'identityName']) if (pupSettings()[k] !== undefined) keep[k] = pupSettings()[k];+ try { fs.mkdirSync(path.dirname(PUP_SETTINGS_PATH), { recursive: true }); fs.writeFileSync(PUP_SETTINGS_PATH, JSON.stringify(keep, null, 2)); } catch (e) {}+ _pupSettings = keep;+ console.log('[settings] restored defaults (dashboard dialog)');+ try { _startModeApply(pupSettings().aumidIcons !== 'on' ? 'plain' : (pupSettings().threadTiles === 'on' ? 'tiles' : 'aumid')); } catch (e) {}+ return done({ ok: true, settings: pupSettings() });+ } if (act === 'set-setting') { // v1.9.259: the dashboard settings dialog. Whitelisted keys only; values validated. const ALLOWED = {@@ -13225,7 +13419,7 @@ const server = http.createServer(async (req, res) => { if (key === 'floatyHitPx') { val = Math.max(6, Math.min(200, Number(val) || 24)); } else if (key === 'fgCaptionMs') {- val = Math.max(500, Math.min(5000, Number(val) || 2000));+ val = Math.max(500, Math.min(12000, Number(val) || 3500)); // v2.0.294 slider goes to 8s; reading-time cap is 12s } else if (!ALLOWED[key] || !ALLOWED[key].includes(val)) { return done({ ok: false, error: 'unknown setting or value' }); }