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.
master
John Lauer
v2.0.721 (insiders): The dashboard stops blinking: each live update now patches only what changed, so window and taskba…
ff1c4b3
27d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047
<!doctype html><html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Pup live windows</title>
<link rel="icon" type="image/svg+xml" href="/dashboard/icon.svg?v=${BRIDGE_VERSION}">
<style>
@import url('https://adom.inc/fonts/fonts.css');
*{box-sizing:border-box} html,body{margin:0;background:#0d1117;color:#c9d1d9;font:14px/1.5 Satoshi,"Segoe UI",system-ui,sans-serif}
/* v2.0.97 (John): brand scrollbars everywhere — the stock chrome bar looked bolted-on in the
settings dialog. Thin, rounded, track invisible, thumb on the border token. */
::-webkit-scrollbar{width:9px;height:9px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:#30363d;border-radius:8px;border:2px solid #0d1117}
::-webkit-scrollbar-thumb:hover{background:#484f58}
::-webkit-scrollbar-corner{background:transparent}
a{color:#64abff}
header{position:sticky;top:0;z-index:10;background:#161b22;border-bottom:1px solid #30363d;height:44px;padding:0 16px;display:flex;gap:10px;align-items:center}
/* ── DOCK CONTRACT regions ───────────────────────────────────────────────────────────────────
The newbie panel is NO LONGER hand-rolled here. adom-ui-design 2.0.0 ships it as ONE shared
module and pup mounts it (see /dashboard/newbie-panel.js). Its CSS, markup, icons, clipboard
path, send path, toasts, tooltips and open/closed memory all live in the module. Every rule the
old hand-rolled panel declared is DELETED rather than left beside the module: a stray .btn or
.ibtn declared after the module's rules is exactly how other apps silently lost their styling to
source order. What remains here is the status pill row, which is its own contract region. */
/* THE FLEET GUTTER (adom-ui-design 2.4.0). ONE number, declared once, used by the panel's mount
container AND by every region under it, so they share an edge because there is a single source
of truth rather than because two literals happen to match. That "by luck rather than by rule"
distinction was my own observation about this page and it is now a fleet rule.
pup was full-bleed at ZERO a side, not the ~4px it looked like: #newbie is a direct child of
body with no padding, so the panel measured exactly clientWidth and the 9px gap from the 744
pane was entirely the scrollbar. selfCheck could not see it while it only measured inside the
panel.
2.5.0 no longer cares WHICH element carries the padding: it finds the nearest scrolling ancestor
and checks that the panel sits 16px inside it on both sides. The contract is where the edges
land, not the structure. (Earlier versions named an element and were wrong twice: measuring
documentElement failed correct app-shell pages by a scrollbar width, and measuring the mount div
would have red-lined every page that carried the gutter on an outer container.)
pup stays with the TOKEN rather than nesting, and that is now the fleet ruling for this shape:
the pill row and filter bar below carry FULL-BLEED bottom borders as separators, and a rule that
spans the pane reads as a divider between regions, while an inset one reads as a card edge. The
panel is the only card in that stack, so the separators stay full width and the gutter is shared
through one token instead. */
:root{--gutter:16px}
#newbie{padding:0 var(--gutter)}
.ledrow{display:flex;flex-wrap:wrap;gap:10px;padding:12px var(--gutter);border-bottom:1px solid #30363d}
.led-pill{display:inline-flex;align-items:center;gap:7px;background:#161b22;border:1px solid #30363d;border-radius:999px;padding:4px 12px;font-size:12.5px;cursor:default}
.led-pill .d{width:8px;height:8px;border-radius:50%;flex:none}
.led-pill .d.ok{background:#3fb950}.led-pill .d.warn{background:#d29922}.led-pill .d.fail{background:#f85149}
.led-pill .lb{font-weight:600;color:#e6edf3}.led-pill .sh{color:#8b949e}
#recall[aria-pressed="true"]{color:#00b8b1;background:#21262d}
/* 620px, NOT 760px (adom-ui-design 1.25.0). Measured dock panes run about 744px to 853px, so a
760px breakpoint sat INSIDE that band and the two-column panel collapsed in the very place it
was designed for: John's pane is 744px, so he never once saw the layout this panel specifies.
620px clears the whole pane band while still collapsing on a phone (390px). */
@media(max-width:620px){.nb-cols{grid-template-columns:1fr}.nb-right{border-left:0;border-top:1px solid #30363d;padding-left:0;padding-top:16px}.play{width:100%;justify-content:center}}
.brand{position:relative;display:flex;align-items:center;gap:7px;color:#00b8b1;font-family:"Familjen Grotesk",Satoshi,sans-serif;font-weight:600;font-size:14px;cursor:default}
.brand .bicon{width:16px;height:16px;color:#e6edf3}
.brand .bsub{color:#8b949e;font-weight:500}
.vsep{width:1px;height:22px;background:#30363d;flex:0 0 1px}
.subject{display:flex;align-items:baseline;gap:10px;min-width:0;flex:1 1 auto;overflow:hidden}
.sname{font-weight:700;color:#e6edf3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.smeta{color:#8b949e;font-size:12px;font-family:"JetBrains Mono",monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.smeta b{color:#c9d1d9;font-weight:500}
@media(max-width:720px){.smeta{display:none}}
.acts2{margin-left:auto;display:flex;gap:6px}
.ibtn{width:30px;height:30px;display:flex;align-items:center;justify-content:center;background:transparent;border:1px solid #30363d;border-radius:8px;color:#e6edf3;cursor:pointer;transition:background .15s ease}
.ibtn:hover{background:#1c2128;border-color:#00b8b1}
.ibtn.danger:hover{border-color:#f85149;color:#f85149}
.ibtn svg{width:16px;height:16px}
.pop{position:fixed;z-index:99999;background:#21262d;border:1px solid #30363d;border-radius:8px;padding:10px 12px;font-size:12px;color:#c9d1d9;font-weight:400;font-family:Satoshi,sans-serif;display:none;box-shadow:0 8px 24px rgba(0,0,0,.5);pointer-events:auto;line-height:1.7}
.pop.show{display:block}
#bar{display:flex;gap:10px;align-items:center;padding:8px 12px;background:#0d1117;border-bottom:1px solid #30363d;flex-wrap:wrap}
.dot{width:8px;height:8px;border-radius:50%;background:#3fb950;display:inline-block;margin-right:6px}
.dot.stale{background:#d29922}
.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}
.btn{background:#16222c;border:1px solid #2a3a47;color:#cfe0ea;border-radius:6px;padding:4px 10px;font-size:12px;cursor:pointer}
.btn:hover{background:#1d2b36;border-color:#00b8b1}
.btn.sm{padding:2px 8px;font-size:11px}
.btn.xs{padding:0 6px;font-size:11px;line-height:16px}
.btn.danger{border-color:#5c2b2b;color:#ff9d95}
.btn.danger:hover{background:#2a1717;border-color:#f85149}
.sub{margin-left:auto;color:#7d8f9f;font-size:12px}
.loops{margin:18px 0 6px;padding:10px 12px;border:1px solid #30363d;border-radius:8px;background:#161b22;font:11.5px ui-monospace,monospace;color:#8b949e}
.lpt{color:#7d8f9f;margin:2px 0 6px}
.lp{display:inline-block;margin:0 10px 6px 0;padding:2px 7px;border:1px solid #30363d;border-radius:6px;color:#c9d1d9}
.lp b{color:#00b8b1}
.lp.hot{border-color:#f85149;color:#f85149}
.lp.hot b{color:#f85149}
/* taskbar strip: mimic of the real Windows taskbar */
#toasts{position:fixed;bottom:18px;left:50%;transform:translateX(-50%);z-index:99999;display:flex;flex-direction:column-reverse;gap:8px;align-items:center}
.toast{background:#21262d;border:1px solid #30363d;border-left:3px solid #00b8b1;color:#c9d1d9;border-radius:8px;padding:8px 14px;font-size:13px;box-shadow:0 8px 24px rgba(0,0,0,.5);transition:opacity .3s ease}
.toast.success{border-left-color:#3fb950}
.toast.error{border-left-color:#f85149}
.toast.out{opacity:0}
/* ===== 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;max-width:270px;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:auto;max-width:100%;height:auto;margin:0 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}
.texwrap{margin-top:10px}
.texstrip{display:flex;flex-wrap:wrap;gap:12px;padding:12px 10px;background:#0d1117;border:1px solid #30363d;border-radius:8px}
.tex{display:flex;flex-direction:column;align-items:center;gap:5px;width:64px}
.tex canvas{width:44px;height:44px;border-radius:7px;display:block}
.texn{font:400 9.5px ui-monospace,monospace;color:#8b949e;text-align:center;line-height:1.2;word-break:break-word}
.texcap{margin-top:6px;font-size:11px;color:#8b949e}
.texwide{width:auto}
.texbase{width:44px;height:44px;border-radius:7px;display:block}
.texmatrix{display:grid;grid-template-columns:repeat(3,minmax(70px,1fr));gap:10px;margin-bottom:8px}
.mcell{position:relative;display:flex;flex-direction:column;align-items:center;gap:5px;padding:8px 4px;border:1px solid #30363d;border-radius:8px;cursor:pointer}
/* v2.0.266 (John: "why is there some random 'default' pill in the upper right corner?"): this was
position:absolute inside .stlbl, which is NOT positioned — so it escaped all the way out to #dlg
and parked itself on top of the dialog's X button. An absolutely-positioned badge that depends on
an ancestor nobody made relative is a trap; make it a plain INLINE chip in the label instead, where
it reads as "1. tiles + overlay (current) default" and has no positioning to get wrong. */
.mdef{display:inline-block;margin-left:6px;vertical-align:1px;font:600 8.5px 'Familjen Grotesk',sans-serif;color:#00b8b1;border:1px solid #00b8b1;border-radius:4px;padding:0 4px;letter-spacing:.04em;white-space:nowrap}
.mcell:hover{border-color:#8b949e}
.mcell.mcur{border-color:#00b8b1;box-shadow:0 0 0 1px #00b8b1 inset}
.mic{position:relative;display:inline-block}
.mic canvas.mbase,.mic canvas[data-mtile],.mic canvas[data-tname],.mic img.mbase{width:44px;height:44px;border-radius:7px;display:block}
.mic .mbadge{position:absolute;top:-7px;right:-7px;width:20px;height:20px;border-radius:5px}
.texnames{display:flex;flex-wrap:wrap;gap:12px;padding-top:10px;border-top:1px solid #30363d}
.texdim{opacity:.45}
.st6{display:flex;flex-direction:column;gap:8px}
/* v2.0.266: flex-wrap + position:relative. Measured live: this row's content wanted 516px inside a
439px strip, so the sample icons spilled past .texstrip's dark background and drew a hard vertical
seam down the panel ("why is the background behind the taskbar identity choices so whacked"). */
.strow6{position:relative;display:flex;align-items:center;flex-wrap:wrap;gap:12px;padding:7px 10px;border:1px solid #30363d;border-radius:8px;cursor:pointer}
.strow6:hover{border-color:#8b949e}
.strow6.mcur{border-color:#00b8b1;box-shadow:0 0 0 1px #00b8b1 inset}
.stlbl{flex:0 0 150px;font:600 12px 'Familjen Grotesk',sans-serif;color:#c9d1d9;line-height:1.35}
.sticons{flex:1 1 220px;min-width:0;display:flex;gap:10px;flex-wrap:wrap;align-items:center;padding:7px 7px 0 0}
.mbcard{position:absolute;top:-7px;right:-7px;width:20px;height:20px;border-radius:5px;background:#202124;display:flex;align-items:center;justify-content:center;overflow:hidden}
.mbcard img{width:16px;height:16px;object-fit:contain}
.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 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}
.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}
.tbi.ghost{color:#5c6c7c;font-size:18px;font-weight:700;border-style:dashed;border-color:#7c3a3a}
.tbi:hover{border-color:#00b8b1;background:#12202a}
.tbi.fg{border-color:#00b8b1}
.tbi img.base{width:30px;height:30px;object-fit:contain}
.tbi img.ov{position:absolute;top:1px;right:1px;width:17px;height:17px;object-fit:contain;border-radius:4px}
.tbi .n{position:absolute;bottom:-6px;left:-6px;background:#00b8b1;color:#05221f;font-size:10px;font-weight:700;border-radius:8px;min-width:16px;height:16px;line-height:16px;text-align:center;padding:0 3px}
.tbi .u{position:absolute;bottom:2px;left:50%;transform:translateX(-50%);width:16px;height:3px;border-radius:2px;background:#00b8b1}
/* tree */
main{padding:14px 18px}
.node{margin:0}
.tw{margin-left:0}
.row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:7px}
.row:hover{background:#0f1720}
.tog{width:16px;flex:0 0 16px;color:#7d8f9f;cursor:pointer;user-select:none;text-align:center}
.tog.leaf{visibility:hidden}
.ico{width:22px;height:22px;flex:0 0 22px;object-fit:contain;border-radius:5px;background:#0f1720}
.ico.sm{width:16px;height:16px;flex:0 0 16px}
.fttray{display:inline-flex;gap:3px;align-items:center;flex-wrap:wrap}
.ft{font-size:9px;line-height:1;padding:2px 5px;border-radius:4px;background:#161f29;color:#9fb2c2;border:1px solid #24303c;white-space:nowrap}
.ft.dim{opacity:.55}
.ft.k-ok{background:#0f2a1c;color:#5fe0a0;border-color:#1c4a33}
.ft.k-gen{background:#2a230f;color:#e0c15f;border-color:#4a3d1c}
.ft.k-red{background:#2a0f14;color:#ff8a95;border-color:#4a1c24}
.ft.k-dark{background:#12202e;color:#7fc0ff;border-color:#1c3a52}
.ft.k-light{background:#26240f;color:#e8e07a;border-color:#48431c}
.ft.k-warn{background:#2a1a0f;color:#ffab6a;border-color:#4a2f1c}
.led{display:inline-block;width:9px;height:9px;border-radius:50%;flex:0 0 9px;vertical-align:middle;box-shadow:0 0 0 1px rgba(0,0,0,.4) inset}
.led.green{background:#22e07a;box-shadow:0 0 6px 1px rgba(34,224,122,.65)}
.led.amber{background:#f0b429;box-shadow:0 0 6px 1px rgba(240,180,41,.5)}
.led.red{background:#ff5b6a;box-shadow:0 0 6px 1px rgba(255,91,106,.6)}
.tbi .led{position:absolute;bottom:-5px;right:-5px;box-shadow:0 0 0 2px #0d1117}
.icostack{position:relative;flex:0 0 22px;width:22px;height:22px}
.icostack .ico{position:absolute;inset:0}
.icostack .icoov{position:absolute;top:-3px;right:-3px;width:13px;height:13px;object-fit:contain;border-radius:3px}
.nm{font-weight:600}
.nm.dim{font-weight:400;color:#c3d0da}
.meta{color:#7d8f9f;font-size:12px}
.tag{font-size:11px;border-radius:5px;padding:1px 7px;border:1px solid #23303c;color:#9fb0c0}
.tag.fg{background:#00b8b11a;border-color:#00b8b1;color:#00b8b1}
.tag.ok{background:#3fb9501a;border-color:#3fb950;color:#3fb950}
.tag.hf{background:#d299221a;border-color:#d29922;color:#e3b341}
.tag.warn{background:#f851491a;border-color:#f85149;color:#ff7b72}
.tag.vp{background:#a371f71a;border-color:#a371f7;color:#c297ff}
.acts{margin-left:auto;display:flex;gap:4px;opacity:0;transition:opacity .1s}
.row:hover .acts{opacity:1}
.tbnode{border:1px solid #1d2833;border-radius:10px;margin-bottom:12px;background:#0c131a;overflow:hidden}
.tbnode>.row{background:#111b23;border-bottom:1px solid #1d2833;border-radius:0}
.win{margin:2px 0 2px 20px;border-left:2px solid #1a2530}
.win>.row .nm{font-size:13px}
.tabs{margin:0 0 4px 44px}
.why{margin:4px 0 8px 44px;padding:6px 9px;background:#0d1117;border:1px solid #1d2833;border-radius:6px;color:#8fa3b3;font-size:11px;line-height:1.6}
.why b{display:block;color:#00b8b1;margin-bottom:2px}
.empty{color:#7d8f9f;padding:40px;text-align:center}
.hidden{display:none}
footer{color:#5c6c7a;font-size:11px;padding:8px 18px 22px}
</style></head><body>
<header>
<div class="brand" id="brand">
<svg class="bicon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="2.75" y="4.5" width="18.5" height="15" rx="2.5"/><path d="M2.75 8.75h18.5"/><circle cx="12" cy="14.2" r="3.1"/></svg><!-- v2.0.339: the header mark is now the SAME drawing as the favicon and the dock card (docs/img/dock-pup.svg): window frame + aperture ring. It used to be the retired two-windows-plus-heartbeat concept, so this page showed one pup mark in its header and a different one in its tab. stroke stays currentColor here and ONLY here: an inline mark should inherit the header colour, while the favicon is an isolated document where currentColor resolves to black, which is why /dashboard/icon.svg hardcodes #e6edf3. -->
<span class="bname">Pup</span><span class="bsub">· live windows</span>
</div>
<div class="pop" id="pop">
<div>version <b id="cV">?</b></div>
<div id="popMeta">connecting</div>
<div><a href="https://wiki.adom.inc/adom/pup-bridge" target="_blank" rel="noopener">wiki: pup-bridge</a></div>
</div>
<div class="vsep"></div>
<div class="subject">
<span class="sname"><span class="dot" id="live"></span><span id="subjName">all windows</span></span>
<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="recall" aria-pressed="true" title="Hide the prompts panel" aria-label="Hide the prompts panel"><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="9"/><path d="M9.6 9.2a2.5 2.5 0 1 1 3.2 3.2c-.5.2-.8.7-.8 1.3v.3"/><path d="M12 17.2h.01"/></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>
<button class="ibtn danger" data-act="close-all" data-confirm="Close ALL pup windows?" data-tip="Close ALL pup windows on this desktop (every window in the list below, not this dashboard page). Asks before doing it." aria-label="close all pup windows"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="8" width="13" height="11" rx="1.6"/><path d="M4 15V6a1.5 1.5 0 0 1 1.5-1.5H16"/><path d="M12.2 11.2l4.6 4.6M16.8 11.2l-4.6 4.6"/></svg></button>
</div>
</header>
<div id="newbie"></div>
<div class="ledrow" id="leds"></div>
<div id="bar">
<span class="pill" id="cM" style="display:none">minimal-touch</span>
<select id="f"><option value="">all AI threads</option></select>
<input id="q" placeholder="filter url / window / ai-thread..." size="20">
</div>
<div id="toasts"></div>
<div id="dlgWrap" class="hidden">
<div id="dlgBg"></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>
<main id="m"><div class="empty">connecting...</div></main>
<div id="loops" class="loops"></div>
<footer id="ft"></footer>
<script src="/dashboard/newbie-panel.js"></script>
<script>
const _q=s=>document.querySelector(s); let SNAP=null; const COLL=new Set();
// v1.9.258 (John hovered a tooltip that his stale page could not render): the page's code is baked
// at load, but the bridge updates underneath it on every ship. When the SSE reports a different
// bridge version than this page was served by, reload once - the dashboard always follows the bridge.
const PAGE_VERSION='${BRIDGE_VERSION}';
const ago=ms=>{if(ms==null)return'-';const s=Math.round(ms/1000);if(s<60)return s+'s';const m=Math.round(s/60);if(m<60)return m+'m';const h=Math.floor(m/60);return h+'h '+(m%60)+'m';};
const esc=t=>String(t==null?'':t).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
// v1.9.311 (John: "why do i keep seeing these refresh blip non-stop?"): the badge URL used to
// cache-bust with SNAP.ts, which changes every SSE tick, so the no-store image refetched a couple
// times a SECOND — a constant flicker. Version it by the badge's actual PAINT TIME instead: the URL
// only changes when the overlay is genuinely repainted, so between repaints the browser serves the
// image from cache and there is zero blip even though render() rebuilds the DOM each tick.
const badge=sid=>{var v=0;try{var w=(SNAP&&SNAP.windows||[]).find(function(x){return x.sessionId===sid;});v=(w&&w.overlayWhy&&w.overlayWhy.at)||0;}catch(e){}return '/dashboard/badge?session='+encodeURIComponent(sid)+'&v='+v;};
const fav=(sid,tid)=>'/dashboard/favicon?session='+encodeURIComponent(sid)+'&tab='+encodeURIComponent(tid);
// v1.9.238: per-tab favicon acquisition state as compact chips + a full tooltip.
function favTags(f){
if(!f) return '<span class="ft dim" title="favicon not resolved yet">fav ?</span>';
const K={'page-favicon':['favicon','k-ok'],'sibling-tab-favicon':['sibling','k-ok'],'favicon.ico':['favicon.ico','k-ok'],'app-provided':['app icon','k-ok'],'generic-web':['generic web','k-gen'],'generic-pdf':['generic pdf','k-gen'],'none':['none','k-red']};
const kd=K[f.kind]||[f.kind||'?','k-gen'];
const bits=[];
bits.push('<span class="ft '+kd[1]+'">'+esc(kd[0])+'</span>');
if(f.format) bits.push('<span class="ft">'+esc(String(f.format).toUpperCase())+'</span>');
if(f.mode&&f.mode!=='default') bits.push('<span class="ft '+(f.mode==='dark'?'k-dark':f.mode==='light'?'k-light':'')+'">'+esc(f.mode)+'-mode</span>');
if(f.transparent===true) bits.push('<span class="ft'+(f.darkDominant?' k-warn':'')+'">transparent'+(f.darkDominant?' · dark':'')+'</span>');
else if(f.transparent===false) bits.push('<span class="ft">opaque</span>');
const tip=['kind: '+(f.kind||'?'),'format: '+(f.format||'?'),'color-scheme: '+(f.mode||'?'),'transparent: '+(f.transparent===null?'n/a':f.transparent),f.luminance!=null?'luminance: '+f.luminance+(f.darkDominant?' (dark)':''):'',f.via?'fetched via: '+f.via:'',f.candidates!=null?'candidates seen: '+f.candidates:'',f.href&&!/^generic|^app-provided/.test(f.href)?'src: '+f.href:'',f.note?'warning: '+f.note:''].filter(Boolean).join(String.fromCharCode(10));
return '<span class="fttray" title="'+esc(tip)+'">'+bits.join('')+'</span>';
}
// 2.0.721 (John on CRR: "i keep seeing it blink like every second"): every snapshot used to replace these lists
// with innerHTML, so every icon <img> was recreated and reloaded every 2 s. morph() patches the live DOM against
// the new markup instead: unchanged nodes and images stay as they are, only changed text and attributes move.
function morph(el,html){const t=document.createElement(el.tagName);t.innerHTML=html;morphKids(el,t);}
function morphKids(a,b){
const bk=Array.from(b.childNodes);
bk.forEach((bn,i)=>{const an=a.childNodes[i];
if(!an){a.appendChild(bn);return;}
if(an.nodeType!==bn.nodeType||an.nodeName!==bn.nodeName){a.replaceChild(bn,an);return;}
if(an.nodeType!==1){if(an.nodeValue!==bn.nodeValue)an.nodeValue=bn.nodeValue;return;}
const img=an.tagName==='IMG', srcChanged=img&&an.getAttribute('src')!==bn.getAttribute('src');
// a broken icon hidden by the error handler keeps its style until its src changes
for(const at of Array.from(an.attributes)) if(!bn.hasAttribute(at.name)&&!(img&&at.name==='style'&&!srcChanged)) an.removeAttribute(at.name);
for(const at of Array.from(bn.attributes)) if(an.getAttribute(at.name)!==at.value) an.setAttribute(at.name,at.value);
morphKids(an,bn);
});
while(a.childNodes.length>bk.length) a.removeChild(a.lastChild);
}
function render(){
if(!SNAP)return; const d=SNAP;
if(d.bridge&&d.bridge.version&&d.bridge.version!==PAGE_VERSION){location.reload();return;}
_q('#cW').textContent=d.counts.windows; _q('#cT').textContent=d.counts.tabs;
_q('#cB').textContent=(d.taskbarButtons||[]).length; _q('#cV').textContent=d.bridge.version;
(function(){const ws=(d.windows||[]).filter(w=>w.sessionId!=='pup-dashboard');const ay=ws.filter(w=>w.profile==='adom-you').length;_q('#cAY').textContent=ay+'/'+ws.length;})();
_q('#popMeta').textContent=d.bridge.platform+' \u00b7 bridge up '+ago(d.bridge.uptimeMs)+' \u00b7 deps '+(d.bridge.depsOk?'ok':'missing');
_q('#subjName').textContent=(_q('#f').value||'all windows');
for(const t of (d.toasts||[])){ if(t.id>SEENTOAST){SEENTOAST=t.id;toast(t.msg,t.type);} }
_q('#cM').style.display=d.bridge.minimalTouch?'':'none';
// header overlay-match rollup: red if ANY button bare, amber if any generic, else green
const bl=(d.taskbarButtons||[]).map(b=>b.led||'red');
let roll=bl.includes('red')?'red':bl.includes('amber')?'amber':(bl.length?'green':'red');
if((d.untrackedWindows||[]).length) roll='red'; // an unmodeled real button = the mimic CANNOT claim MATCH
_q('#cLedDot').className='led '+roll;
_q('#cLedTxt').textContent=(d.untrackedWindows||[]).length?((d.untrackedWindows||[]).length+' UNTRACKED'):roll==='green'?'MATCH':roll==='amber'?'generic':'MISMATCH';
_q('#age').textContent='updated '+new Date(d.ts).toLocaleTimeString()+(d.regen?(d.regen.running?' · regenerating '+d.regen.inFlight+' windows…':(d.regen.changed!=null?' · last sweep: '+d.regen.changed+'/'+d.regen.swept+' corrected':'')):'');
const sel=_q('#f'),cur=sel.value;
if(sel.options.length-1!==d.threads.length){sel.innerHTML='<option value="">all AI threads</option>'+d.threads.map(t=>'<option>'+esc(t)+'</option>').join('');sel.value=cur;}
const q=_q('#q').value.toLowerCase(),th=sel.value;
const byId=Object.fromEntries(d.windows.map(w=>[w.sessionId,w]));
const match=w=>{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;};
const groups=(d.taskbarButtons||[]).map(b=>({...b,wins:b.windows.map(s=>byId[s]).filter(w=>w&&match(w))})).filter(g=>g.wins.length);
// taskbar strip (real order, real badge)
// v1.9.235 (John's audit): each strip button mimics the REAL taskbar button - the browser/category
// BASE icon full-size, with the painted overlay badge in the TOP-RIGHT corner at ~55% (measured
// from his actual taskbar), and NO overlay at all when pup has not painted one (a bare Chrome
// button must render bare here too, otherwise the mimic lies).
const ghosts=(d.untrackedWindows||[]).map(h=>'<div class="tbi ghost" title="a window of the pup-managed Chrome is on the real taskbar but pup has NO window record for it (hwnd '+h+'). The mimic cannot know its icon - this is why the header LED is red.">?<span class="led red"></span></div>').join('');
morph(_q('#taskbar'),'<span class="lbl" title="mimics ONLY the taskbar buttons of pup-managed Chrome windows. Other Adom apps (Adom Hydrogen, Adom Bridge) have their own teal-tile buttons that are out of pup scope.">TASKBAR (pup)</span>'+groups.map((g,i)=>{
const fg=g.wins.some(w=>w.foreground);
return '<div class="tbi'+(fg?' fg':'')+'" data-jump="g'+i+'" title="'+esc(g.label)+': '+esc(g.ledWhy||'')+'">'
+'<img class="base" src="'+esc(g.baseIcon||'')+'" data-fallback="hide">'
+(g.badgeSession?'<img class="ov" src="'+badge(g.badgeSession)+'" data-fallback="hide">':'')
+'<span class="led '+esc(g.led||'red')+'" title="'+esc(g.ledWhy||'')+'"></span>'
+(g.wins.length>1?'<span class="n">'+g.wins.length+'</span>':'')
+(fg?'<span class="u"></span>':'')+'</div>';
}).join('')+ghosts);
// tree: taskbar icon -> windows -> tabs
morph(_q('#m'),groups.length?groups.map((g,gi)=>{
const gid='g'+gi, gcoll=COLL.has(gid);
return '<div class="tbnode" id="'+gid+'">'
+'<div class="row"><span class="tog" data-toggle="'+gid+'">'+(gcoll?'▸':'▾')+'</span>'
+'<span class="icostack"><img class="ico" src="'+esc(g.baseIcon||'')+'" data-fallback="hide">'
+(g.badgeSession?'<img class="icoov" src="'+badge(g.badgeSession)+'" data-fallback="hide">':'')+'</span>'
+'<span class="led '+esc(g.led||'red')+'" title="overlay match: '+esc(g.ledWhy||'')+'"></span>'
+'<span class="nm">'+esc(g.label)+'</span>'
+'<span class="meta">'+g.wins.length+' window'+(g.wins.length>1?'s':'')+' · '+esc(g.grouping)+' · badge from '+esc(g.badgeVia||'?')+'</span>'
+'<span class="acts"><button class="btn sm" data-act="flash-group" data-ids="'+esc(g.windows.join(','))+'">flash</button>'
+'<button class="btn sm" data-act="unflash-group" data-ids="'+esc(g.windows.join(','))+'">clear</button></span></div>'
+'<div class="tw'+(gcoll?' hidden':'')+'">'+g.wins.map(w=>{
const wid=gid+'-'+esc(w.sessionId), wcoll=COLL.has(wid);
const crash=w.tabs.some(t=>t.crashed)||w.lostBrowser;
return '<div class="win">'
+'<div class="row"><span class="tog" data-toggle="'+wid+'">'+(wcoll?'▸':'▾')+'</span>'
+'<span class="led '+esc(w.overlayLed||'red')+'" title="'+(w.overlayLed==='green'?'real page favicon painted — matches taskbar':w.overlayLed==='amber'?'painted, generic fallback (page has no favicon)':'NOT painted — taskbar would be bare/stale')+'"></span>'
+'<img class="ico" src="'+badge(w.sessionId)+'" data-fallback="hide">'
+'<span class="nm">'+esc(w.sessionId)+'</span>'
+'<span class="tag '+(w.foreground?'fg':'')+'">'+(w.foreground?'FG':'bg')+'</span>'
+(w.sessionId==='pup-dashboard'?'<span class="tag">dashboard</span>':(w.profile==='adom-you'?'<span class="tag ok" title="surfing on the shared adom-you profile: every login this window makes enriches the shared credential store, so pup can auto-fill it everywhere later">surfing as adom-you</span>':'<span class="tag warn" title="NOT on adom-you: logins made here do NOT build the shared credential store. Prefer adom-you unless isolation is genuinely needed.">NOT adom-you: '+esc(w.profile)+'</span>'))
+(w.owner?'<span class="tag">'+esc(w.owner)+(w.ownerSource==='caller-identity'?' ✓':'')+'</span>':'<span class="tag warn">UNOWNED</span>')
+(w.wiki?'<span class="tag '+(w.wiki.authed?'ok':'warn')+'" title="'+(w.wiki.authed?'This Adom wiki window is LOGGED IN — you see private source, drafts, owner-only cards, reply/edit':'This Adom wiki window is the PUBLIC / logged-OUT view — exactly what the world sees')+'">🔑 wiki: '+(w.wiki.authed?'signed in':'public')+'</span>':'')
+(w.highFps?'<span class="tag hf">highFps '+w.highFpsMinutesLeft+'m</span>':'')
+(w.emulatedViewport?'<span class="tag vp">vp '+w.emulatedViewport.width+'x'+w.emulatedViewport.height+'</span>':'')
+(crash?'<span class="tag warn">'+(w.lostBrowser?'browser lost':'crashed tab')+'</span>':'')
+(w.errorCount?'<span class="tag warn" title="'+esc((w.recentErrors||[]).map(e=>'['+e.type+'] '+e.text).join(String.fromCharCode(10))||'no detail captured')+'">'+w.errorCount+' err</span>':'')
+'<span class="meta">'+esc(w.profile)+' · '+ago(w.uptimeMs)+'</span>'
+'<span class="acts">'
+'<button class="btn sm" data-act="fg" data-sid="'+esc(w.sessionId)+'">bring to fg</button>'
+'<button class="btn sm" data-act="bg" data-sid="'+esc(w.sessionId)+'">send to bg</button>'
+'<button class="btn sm" data-act="flash" data-sid="'+esc(w.sessionId)+'">flash</button>'
+'<button class="btn sm" data-act="unflash" data-sid="'+esc(w.sessionId)+'">clear</button>'
+'<button class="btn sm danger" data-act="close-window" data-sid="'+esc(w.sessionId)+'" data-confirm="Close '+esc(w.sessionId)+'?">close</button></span></div>'
+'<div class="'+(wcoll?'hidden':'')+'">'
+(w.foreground&&w.foregroundReason?'<div class="why"><b>why foreground</b>'+esc(w.foregroundReason)+'</div>':'')
+(w.overlayWhy?'<div class="why"><b>how this icon was chosen</b><span class="led '+esc(w.overlayLed||'red')+'"></span> '+(w.overlayLed==='green'?'real page favicon':w.overlayLed==='amber'?'generic fallback (no favicon on the page)':'not painted')+'<br>tab '+esc(w.overlayWhy.tabId||'?')+', '+esc(w.overlayWhy.tabPickedBy)+'<br>source: '+esc(w.overlayWhy.source||'none')+'<br>via: '+esc(w.overlayWhy.fetchedVia)+'<br>painted by: '+esc(w.overlayWhy.paintedBy)+'</div>':'')
+'<div class="tabs">'+w.tabs.map(t=>
'<div class="row"><span class="tog leaf">•</span>'
+'<img class="ico sm" src="'+fav(w.sessionId,t.tabId)+'" data-fallback="hide">'
+'<span class="nm dim'+(t.active?'"':'"')+'">'+esc(t.title||t.url||'-')+'</span>'
+(t.active?'<span class="tag fg">active</span>':'')
+(t.crashed?'<span class="tag warn">crashed</span>':'')
+favTags(t.fav)
+'<span class="acts"><button class="btn xs" data-act="fg" data-sid="'+esc(w.sessionId)+'" data-tid="'+esc(t.tabId)+'">fg</button>'
+'<button class="btn xs" data-act="bg" data-sid="'+esc(w.sessionId)+'">bg</button>'
+'<button class="btn xs danger" data-act="close-tab" data-sid="'+esc(w.sessionId)+'" data-tid="'+esc(t.tabId)+'">close</button></span></div>'
).join('')+'</div></div></div>';
}).join('')+'</div></div>';
}).join(''):'<div class="empty">no pup windows open'+(th||q?' matching this filter':'')+'</div>');
_q('#ft').textContent='bridge up '+ago(d.bridge.uptimeMs)+' \u00b7 '+d.bridge.platform+' \u00b7 deps '+(d.bridge.depsOk?'ok':'MISSING')+' \u00b7 win32 '+(d.bridge.win32InProcess?'in-process':'powershell fallback');
// v2.0.315 LOOP RADAR (John): the periodic loops and in-process Win32 calls no longer show in ab's
// activity log, so they are shown HERE, per minute, with the declared cadence next to each so a
// spamming loop stands out (ticks/min far above 60000/cadence).
try{
const lp=d.bridge.loops||{}; const w=d.bridge.win32Calls||{};
const rows=Object.entries(lp).map(([n,v])=>{const exp=Math.max(1,Math.ceil(60000/v.cadenceMs));const hot=v.lastMin>exp*1.5;return '<span class="lp'+(hot?' hot':'')+'" title="declared cadence '+v.cadenceMs+'ms = '+exp+'/min; avg '+v.avgMs+'ms, max '+v.maxMs+'ms, '+v.ticks+' ticks total, '+v.slowTicks+' slow">'+n+' <b>'+v.lastMin+'</b>/'+exp+'</span>';}).join('');
const wc=Object.entries(w).map(([n,v])=>n+'='+v).join(' ');
morph(_q('#loops'),'<div class="lpt">loops, ticks in the last minute (actual/declared)</div>'+rows+'<div class="lpt">in-process win32 calls since boot: '+(wc||'none')+' \u00b7 ab enum_windows last minute: '+(d.bridge.abEnumLastMin||0)+'</div>');
}catch(e){}
}
function toast(msg,type){
const t=document.createElement('div');t.className='toast '+(type||'info');t.textContent=msg;
_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','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{
// settings-layout: every settings round trip is BOUNDED (5s), so a stalled bridge can never
// leave a control visually stuck; the click already reflected in the DOM optimistically.
const ac=new AbortController(); const tm=setTimeout(()=>ac.abort(),5000);
const r=await fetch('/dashboard/action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(p),signal:ac.signal}).finally(()=>clearTimeout(tm));
const j=await r.json().catch(()=>({}));
const what=(ACTMSG[p.action]||p.action)+(p.sessionId?' '+p.sessionId:'');
if(p.action==='regen-overlays'){ if(j.ok&&j.started) toast('Regenerating overlays on '+j.swept+' windows: watch the icons update live','info'); else if(j.ok&&j.alreadyRunning) toast('A regenerate sweep is already running','info'); else toast('regenerate failed'+(j.error?': '+j.error:''),'error'); return j; }
if(j.ok) toast(what+(p.action==='unflash-all'&&j.swept!=null?' ('+j.swept+' windows swept)':'')+(p.action==='flash-all'&&j.delivered!=null?' ('+j.delivered+' of '+j.n+' delivered)':''), p.action.indexOf('close')===0?'success':'info');
else toast(what+' failed'+(j.error?': '+j.error:''),'error');
return j;
}catch(e){toast(p.action+' failed: '+e.message,'error');}
}
let SEENTOAST=0;
// proper tooltip (adom-ui-design tooltips skill): body-appended fixed div, 600ms reveal,
// clamped to the viewport on BOTH axes, never a title attr for rich content.
(function(){
let tt=null,show=null;
function hide(){clearTimeout(show);if(tt){tt.remove();tt=null;}}
document.addEventListener('mouseover',ev=>{
// v2.0.97 (John: double tooltips — a raw title attr AND this tooltip): any title attr is
// absorbed into data-tip the first time the cursor arrives, well inside the ~1s native
// delay, so the browser tooltip never fires and every hint renders through this one system.
const t=ev.target.closest('[title]');
if(t){const v=t.getAttribute('title');if(v&&!t.dataset.tip)t.dataset.tip=v;t.removeAttribute('title');}
const el=ev.target.closest('[data-tip]');
if(!el){hide();return;}
clearTimeout(show);
show=setTimeout(()=>{
hide();
tt=document.createElement('div');
tt.style.cssText='position:fixed;z-index:100000;max-width:380px;background:#21262d;border:1px solid #30363d;border-radius:8px;padding:10px 12px;font-size:12px;line-height:1.6;color:#c9d1d9;box-shadow:0 8px 24px rgba(0,0,0,.5);pointer-events:none';
tt.textContent=el.dataset.tip;
document.body.appendChild(tt);
const r=el.getBoundingClientRect();
const M=8,w=tt.offsetWidth,h=tt.offsetHeight;
let x=r.left+14,y=r.bottom+10;
if(y+h>innerHeight-M)y=Math.max(M,r.top-h-10);
x=Math.max(M,Math.min(x,innerWidth-w-M));
y=Math.max(M,Math.min(y,innerHeight-h-M));
tt.style.left=x+'px';tt.style.top=y+'px';
},600);
});
document.addEventListener('scroll',hide,true);
})();
// 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:'taskbarMode',mainsel:1,threadex:1,l:'Taskbar identity',info:'Three ALTERNATE modes for how pup brands your Windows taskbar, from most to least branded. TILES: every AI thread gets ONE button whose icon is the thread NAME drawn on the Adom teal tile - all of that thread\u2019s windows group under it, so the taskbar reads as a list of your threads. AUMID: every pup window gets its own button with a pup/category icon (per-window identity, no thread grouping) - the AUMID grouping and icon-style options below apply to this mode only. PLAIN: pup never touches identity; windows stack under the stock Chrome-for-Testing button like any unbranded app. The favicon overlay badge is separate - the Overlay badges toggle below works in tiles and aumid modes.',d:'Click a row in the matrix below to switch modes; the current one is ringed.',sel:['tiles','aumid','plain']},
// v2.0.269 (John): the badge rows live HERE, directly under Taskbar identity, because they are
// 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 in the top-right corner.',pair:[{img:'obon',cap:'On: the wiki favicon badged on the tile'},{img:'oboff',cap:'Off: the bare thread tile'}]},
{k:'overlaySize',l:'Badge size',d:'How big the badge is drawn, as a percent of full size. Each card is the real tile at that size.',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. Quirk: the FIRST right-click flashes the menu and the second holds. Measured cause (ab#155): a registry-only identity has no Start Menu shortcut for the shell to anchor the list to. The Start Menu anchor toggle below fixes it at the cost of one Start Menu folder.',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,img:'jldashboard'},
{k:'jlAnnotate',l:'',d:'The Annotate this page task: draw on the page; Done captures it for the AI thread that owns the window.',dep:'jumpLists',subtgl:1,img:'jlannotate'},
{k:'aumidShortcut',l:'',d:'Start Menu anchor: write one small visible shortcut per pup identity into a collapsed "Adom Pup" folder in the Start Menu, which is the only thing that makes the jump list stay up on the FIRST right-click (measured: hidden shortcuts and folders do not anchor). Off keeps the Start Menu clean and the first right-click flashes. Entries also show under Recently added and in Start search for a while. Needs ab 2.1.49.',dep:'jumpLists',subtgl:1},
{k:'winAttentionFlash',l:'Windows attention flash on new windows',d:'Windows lights a taskbar button (the attention state FlashWindowEx sets) when a window asks for the foreground and is refused because you are in another app. Off: pup creates every new window without asking, so nothing is refused and nothing lights up. On: Chrome asks as usual, and each new pup window you were not looking at gets the flash until you click it.'},
{k:'annCaptionBar',l:'Caption bar on captures',d:'Bake a thin context strip under every annotation/snip capture (thread, page, time) so any AI you paste it into sees where it came from.',img:'anncapbar'},
{k:'toolbarExtension',l:'Adom toolbar in Chrome',d:'The Adom Pup extension button in the browser toolbar (annotate, snip, view toggle, dashboard, owner, close). Requires the one-time extension install.',img:'toolbarext'},
{k:'tbSnipHotkey',l:'',d:'Alt+Shift+A starts annotate (Auto: capture on mouse-up); Alt+Shift+M starts it in Multi (compose several shapes, capture once on Done).',dep:'toolbarExtension',subtgl:1},
{k:'tbFloatyPencil',l:'',d:'The tiny pencil in the upper-right corner of each page: one click starts annotate. Off by default: the corner hover zone can fight buttons in app headers, and the toolbar pen button covers discovery.',dep:'toolbarExtension',subtgl:1},
{k:'floatyHitPx',l:'',d:'',sliderLabel:'pencil corner hit area',unit:'px',slider:[6,120,2],dep:'tbFloatyPencil'},
// v2.0.293 (John: "why did you make progress bar vs orange flash a child of Favicon overlay
// badges? that hierarchy doesn't make sense"): taskbarActivity is its OWN top-level row here in
// the activity/foreground section, NOT inside the taskbar-identity cluster where its position
// between the badge/AUMID rows rendered it as a sub-setting of overlay badges.
{k:'taskbarActivity',l:'Taskbar activity signal',d:'Pick what the button shows. Each card is a real capture of the same pup button on a dark taskbar.',sel:['progress','flash','off'],selLabels:{'progress':'progress bar (default)','flash':'orange flash (old)','off':'off'},imgBy:{progress:'tbactprogress',flash:'tbactflash',off:'tbactoff'}},
{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.',img:'fgcaption'},
{k:'fgCaptionMs',l:'',d:'',sliderLabel:'minimum caption time (longer text auto-extends at reading speed, 12s max)',unit:'s',slider:[500,8000,250],dep:'fgCaption',test:1},
{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 a matching saved login when a login page loads in a pup window (background, no Chrome dropdown). pup never submits on its own: submitting a stored password is reason-gated and confirms the live domain first, to guard against lookalike/phishing sites.'},
{k:'captureLogins',l:'Capture logins to the vault',d:'As you sign into sites in pup windows, store the username and password in the OS keychain (encrypted at rest, never logged) so return visits auto-fill. This is how the shared adom-you identity grows over time.'},
{k:'reportIssues',l:'Issue-report hints',d:'Let pup invite AIs to file a wiki issue when they hit something broken.'},
{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.',img:'ownerchip'},
{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
// actual decrypt is AD-core (adom/adom-bridge #51) so the Import button reports honest-pending.
let _importScan=null; let _importSel={};
function _idTag(a){return '<span style="display:inline-block;background:rgba(120,150,205,.22);border-radius:4px;padding:1px 6px;margin:0 4px 2px 0;font-size:11px;white-space:nowrap">'+esc(a)+'</span>';}
function _idBadges(accts){return (accts&&accts.length)?accts.map(_idTag).join(''):'<span style="opacity:.55;font-size:11px">no linked account</span>';}
function importSectionHtml(){
const s=_importScan; let rows='';
if(s&&s.profiles){
rows=s.profiles.length?s.profiles.map(p=>{
const key=p.browser+'/'+p.profileDir, on=_importSel[key]!==false;
const cnt=(p.passwordCount==null?'?':p.passwordCount)+' logins';
const lock=p.abe?'<span style="opacity:.6;font-size:11px" title="App-Bound Encrypted - needs admin approval to decrypt">🔒 encrypted</span>':'';
return '<div class="srow" style="padding:8px 0"><label class="stxt" style="cursor:pointer;display:flex;gap:10px;align-items:flex-start;width:100%">'
+'<input type="checkbox" data-imchk="'+esc(key)+'"'+(on?' checked':'')+' style="margin-top:3px;flex:0 0 auto">'
+'<span style="flex:1 1 auto;min-width:0"><div class="slbl">'+esc(p.browserName||p.browser)+' — '+esc(p.profileName||p.profileDir)+'</div>'
+'<div class="sdesc">'+cnt+' '+_idBadges(p.accounts)+' '+lock+'</div></span></label></div>';
}).join(''):'<div class="sdesc">No Chrome / Edge / Brave profiles found on this machine.</div>';
}
const has=s&&s.profiles&&s.profiles.length;
const summary=s?'<div class="sdesc" style="margin:8px 0">Found <b>'+(s.profiles?s.profiles.length:0)+'</b> profile(s), ~<b>'+(s.totalPasswords||0)+'</b> saved logins across '+((s.accounts&&s.accounts.length)?s.accounts.map(esc).join(', '):'no linked accounts')+'.'+(s.pythonAvailable?'':' <span style="opacity:.6">(login counts unavailable - python not found)</span>')+'</div>':'';
return '<div class="srow" style="flex-direction:column;align-items:stretch;border-top:2px solid rgba(255,255,255,.16);margin-top:16px;padding-top:14px">'
+'<div class="slbl" style="font-size:15px">Import logins from Chrome / Edge <span class="ibadge" data-tip="Seeds the shared adom-you profile with the logins you already have, tagged by which account they came from ([email protected] vs [email protected] vs [email protected]) so the agent acts as the right you on each site. Discovery runs now; the actual decrypt needs one Windows admin approval, being built in Adom Bridge core (issue #51).">i</span></div>'
+'<div class="sdesc">Build a comprehensive adom-you profile from every browser profile on this machine. Each login stays tagged by its source account so the agent picks the right identity per site.</div>'
+'<div style="display:flex;gap:8px;margin:12px 0;flex-wrap:wrap"><button class="btn sm" id="importScanBtn">'+(s?'Rescan browsers':'Scan my browsers')+'</button>'
+(has?'<button class="btn sm" id="importRunBtn">Import selected logins</button>':'')+'</div>'
+summary+rows
+(has?'<div id="importStatus" class="sdesc" style="opacity:.6;margin-top:10px">Import pops one Adom Bridge consent dialog + one Windows admin (UAC) prompt, then decrypts your selected logins into the adom-you vault, tagged by account. Passwords are never shown or logged.</div>':'')
+'</div>';
}
async function runImportScan(){const b=_q('#importScanBtn');if(b){b.disabled=true;b.textContent='Scanning…';}const j=await act({action:'import-scan'});_importScan=(j&&j.ok)?j:{profiles:[],accounts:[]};renderDlg();}
async function runImport(){
const sel=((_importScan&&_importScan.profiles)||[]).filter(p=>_importSel[p.browser+'/'+p.profileDir]!==false);
if(!sel.length){toast('Select at least one profile','error');return;}
const btn=_q('#importRunBtn');if(btn){btn.disabled=true;btn.textContent='Approve on your desktop…';}
const el=_q('#importStatus');if(el)el.textContent='Waiting for the Adom Bridge consent dialog and Windows admin (UAC) prompt on your screen…';
const r=await fetch('/dashboard/action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'import-run',profiles:sel.map(p=>({browser:p.browser,profileDir:p.profileDir}))})});
const j=await r.json().catch(()=>({}));
if(btn){btn.disabled=false;btn.textContent='Import selected logins';}
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={winAttentionFlash:'Windows attention flash',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',aumidShortcut:'Start Menu anchor (first-click jump list)',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={winAttentionFlash:'Off',taskbarMode:'Tiles',overlayBadges:'On',overlaySize:'75%, balanced',taskbarGrouping:'Split',aumidIconStyle:'Category',jumpLists:'On',jlViewTasks:'On',jlCloseAll:'On',jlDashboard:'On',jlAnnotate:'On',aumidShortcut:'Off',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:'Off',minimalTouch:'On'};
// choice cards: the option name + the one-line tradeoff + the REAL screenshot per option
const SET_CHOICES={
overlaySize:[{v:'50',n:'50%',d:'Smallest badge, maximum clearance for the tile text.',img:'obsize50',alt:'The pup tile with a small favicon badge in its corner'},
{v:'66',n:'66%',d:'Small, still clear of the text.',img:'obsize66',alt:'The pup tile with a medium-small favicon badge'},
{v:'75',n:'75%',d:'Balanced: readable badge, readable text.',img:'obsize75',alt:'The pup tile with a medium favicon badge'},
{v:'100',n:'100%',d:'Full size. Covers the corner of the tile text.',img:'obsize100',alt:'The pup tile with a large favicon badge overlapping the text corner'}],
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','aumidShortcut']},
]},
{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','winAttentionFlash']},
{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={winAttentionFlash:'Taskbar: ',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: ',aumidShortcut:'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]));
const SET_SHOT_PENDING=new Set(['tbFloatyPencil','titleTag','titlePrefix','notMineCaption']);
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.pair?'<div style="display:flex;gap:.75rem;flex-wrap:wrap">'+def.pair.map(q=>_shot(q.img,q.cap,q.cap)).join('')+'</div>'
:def.img?_shot(def.img,SET_NAMES[k]+' as it appears on this machine',null):(SET_SHOT_PENDING.has(k)?'<div class="shot-pending">Screenshot pending: a real capture of this will be added; nothing is mocked up in its place.</div>':'');
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('#dlgSide').innerHTML=SET_SECTIONS.map(sec=>'<button class="side-sec'+(_dlgSelected===sec.id?' selected':'')+'" data-jump="'+sec.id+'"><span aria-hidden="true" style="font-size:.7em;opacity:.7">\u25BE</span>'+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).
// Uses the threads that actually have windows open right now, then fills out with canonical examples
// so the strip always shows a range of name lengths.
function paintThreadExamples(){
const strip=_q('#texstrip'); if(!strip) return;
// v2.0.204 (John, the final spec): the examples are FIVE FIXED CANONICAL SAMPLES — wiki page,
// dashboard app, ti.com, mouser, digikey — rendered identically under every one of the six
// states, from BUNDLED ground-truth art (each vendor's own favicon, the real category tiles, the
// real Chrome-for-Testing icon). Always present, never dependent on which windows are open: the
// rows exist to teach what the modes MEAN.
const st2=(SNAP&&SNAP.bridge&&SNAP.bridge.settings)||{};
const mode=st2.aumidIcons!=='on'?'plain':(st2.threadTiles==='on'?'tiles':'aumid');
const ovOn=st2.overlayBadges!=='off';
const V='v='+(typeof PAGE_VERSION!=='undefined'?PAGE_VERSION:'0');
// v2.0.206 (John: the wiki has TWO icon styles — signed-in and public — both are canonical
// samples): SIX fixed samples per state row.
const SAMPLES=[
{tile:'wiki in', fav:'/dashboard/sample-favicon?n=wiki&'+V, cat:'wikiin'},
{tile:'wiki out', fav:'/dashboard/sample-favicon?n=wiki&'+V, cat:'wiki'},
{tile:'dash', fav:'/dashboard/icon.svg?'+V, cat:'app'},
{tile:'ti.com', fav:'/dashboard/sample-favicon?n=ti&'+V, cat:'web'},
{tile:'mouser', fav:'/dashboard/sample-favicon?n=mouser&'+V, cat:'web'},
{tile:'digikey', fav:'/dashboard/sample-favicon?n=digikey&'+V, cat:'web'},
];
function fb(sm){ return '<span class="mbcard"><img src="'+sm.fav+'" alt=""></span>'; }
function tileIc(sm,ov){ return '<span class="mic"><canvas width="128" height="128" data-tname="'+esc(sm.tile)+'"></canvas>'+(ov?fb(sm):'')+'</span>'; }
function aumidIc(sm,ov){ return '<span class="mic"><img class="mbase" src="/dashboard/sample-cat?k='+sm.cat+'&'+V+'" data-catfb="'+sm.cat+'">'+(ov?fb(sm):'')+'</span>'; }
// v2.0.205 (John: "I want all 5 icons for each of the 6 states"): plain rows show all five
// samples too — each as the stock Chrome icon (that IS what plain looks like per window), with
// its OWN site badge in the overlay row. The stacking note stays as a caption, not a collapse.
function plainIc(sm,ov){ return '<span class="mic"><img class="mbase" src="/dashboard/baseicon?profile=adom-you&'+V+'">'+(ov?fb(sm):'')+'</span>'; }
function plainGroup(ov){
var cards=SAMPLES.map(function(sm){
return '<span style="display:inline-flex;flex-direction:column;width:44px;height:34px;border:1px solid #3a4a52;border-radius:4px;overflow:hidden;background:#e8eaed;flex:none">'
+'<span style="display:flex;align-items:center;gap:3px;height:12px;background:#202124;padding:0 3px">'
+'<img src="'+sm.fav+'" style="width:8px;height:8px;object-fit:contain">'
+'<span style="flex:1"></span><span style="color:#9aa0a6;font-size:7px;line-height:1">\u2715</span></span>'
+'<span style="flex:1;background:linear-gradient(#ffffff,#e8eaed)"></span></span>';
}).join('');
// v2.0.266: this strip of six 44px flex:none cards did NOT wrap, so it was a hard ~298px floor
// that no amount of shrinking elsewhere could beat — the last 49px of the panel's horizontal
// overflow (and the seam past .texstrip's background) came from exactly here. Let it wrap.
return '<div style="display:flex;flex-direction:column;align-items:center;gap:3px;max-width:100%;min-width:0">'
+'<div style="display:flex;flex-wrap:wrap;justify-content:center;gap:4px;padding:5px 6px;border:1px solid #2f3f46;border-radius:6px;background:#1b2429;max-width:100%">'+cards+'</div>'
+'<span style="color:#4a5a58;font-size:10px;line-height:1.2">\u25B2 what hovering the ONE button shows</span>'
+'<span class="mic"><img class="mbase" src="/dashboard/baseicon?profile=adom-you&'+V+'">'+(ov?fb(SAMPLES[3]):'')+'</span>'
+'</div>';
}
function showApplyProgress(){
// v2.0.214b: a setting click re-renders the whole dialog, destroying anything inserted at
// click time — so the panel re-finds (or re-creates) itself on EVERY poll tick instead.
function ensureEl(){
var el=document.getElementById('applyprog');
if(el&&el.isConnected) return el;
var anchor=document.querySelector('.st6'); if(!anchor) return null;
el=document.createElement('div'); el.id='applyprog';
el.style.cssText='margin:10px 4px 2px;padding:10px 12px;border:1px solid #2a3f3e;border-radius:8px;background:#0f1817;font:12px/1.8 "JetBrains Mono",Consolas,monospace;color:#9fb3b1';
(anchor.parentElement||anchor).insertAdjacentElement('afterend',el);
return el;
}
var t0=Date.now();
function paint(a){
var el=ensureEl(); if(!el) return false;
el.style.display='block';
if(!a || (Date.now()-a.at>60000 && !a.done)){ el.innerHTML='<span style="color:#00b8b1">Applying\u2026</span>'; return false; }
var h='<style>@keyframes apr{to{transform:rotate(360deg)}}.apspin{display:inline-block;animation:apr 1s linear infinite}</style>';
a.steps.forEach(function(st){
var mark = st.state==='done' ? '<span style="color:#3fb950">\u2713</span>'
: st.state==='run' ? '<span class="apspin" style="color:#00b8b1">\u25CC</span>'
: '<span style="color:#4a5a58">\u25CB</span>';
h+='<div>'+mark+' '+st.label+'</div>';
});
if(a.done) h+='<div style="margin-top:6px;color:#3fb950;font-weight:700">\u2713 Done: your taskbar now matches this state</div>';
el.innerHTML=h;
return !!a.done;
}
(function tick(){
fetch('/dashboard/apply-progress').then(function(r){return r.json()}).then(function(j){
var fin=paint(j&&j.apply);
if(fin){ setTimeout(function(){ var e2=document.getElementById('applyprog'); if(e2) e2.style.display='none'; },7000); return; }
if(Date.now()-t0<45000) setTimeout(tick,600);
else { var e3=ensureEl(); if(e3) e3.innerHTML='<span style="color:#d29922">Still settling in the background\u2026 (check the taskbar in a few seconds)</span>'; }
}).catch(function(){ if(Date.now()-t0<45000) setTimeout(tick,900); });
})();
}
const states=[
['tiles',true, '1. tiles + overlay'],
['tiles',false,'2. tiles'],
['aumid',true, '3. aumid + overlay'],
['aumid',false,'4. aumid'],
['plain',true, '5. plain + overlay'],
['plain',false,'6. plain'],
];
strip.innerHTML='<div class="st6">'+states.map(function(cfg,i){
const m=cfg[0],ov=cfg[1],lbl=cfg[2];
const cur=(m===mode&&ov===ovOn);
const isDefault=(m==='tiles'&&ov);
let icons='';
if(m==='tiles'){ icons=SAMPLES.map(function(sm){return tileIc(sm,ov);}).join(''); }
else if(m==='aumid'){ icons=SAMPLES.map(function(sm){return aumidIc(sm,ov);}).join(''); }
else {
var ptxt = ov
? 'ALL pup windows share this ONE stock Chrome button. They stay separate windows: hover the button and Windows fans out a preview of each (drawn above). You just cannot ungroup them on the bar, because the per-app identity that separates buttons is exactly what plain mode removes. The single badge belongs to the most recently updated window.'
: 'ALL pup windows share this ONE stock Chrome button. They stay separate windows: hover the button and Windows fans out a preview of each (drawn above). You just cannot ungroup them on the bar, because the per-app identity that separates buttons is exactly what plain mode removes.';
icons=plainGroup(ov)+'<span class="texn" style="align-self:center;flex:1 1 190px;min-width:0;max-width:100%;text-align:left">'+ptxt+'</span>';
}
return '<div class="strow6'+(cur?' mcur':'')+'" data-si="'+i+'" title="click to switch to this state">'
+'<span class="stlbl">'+lbl+(cur?' (current)':'')+(isDefault?' <span class="mdef">default</span>':'')+'</span>'
+'<div class="sticons">'+icons+'</div></div>';
}).join('')+'</div>';
const cap=strip.parentElement?strip.parentElement.querySelector('.texcap'):null;
if(cap)cap.textContent='Six canonical sample windows (signed-in wiki, public wiki, the pup dashboard app, ti.com, mouser, digikey) rendered under every state so you can compare like for like. Tiles are drawn by the real tile renderer; category icons and the Chrome icon are the real registered art; badges are each site\u2019s own favicon. Click a row to switch to it; a step list shows the apply progress and a green check when your taskbar matches.';
strip.querySelectorAll('canvas[data-tname]').forEach(function(cv){ drawNameTile(cv.getContext('2d'),128,cv.getAttribute('data-tname')); });
strip.querySelectorAll('img[data-catfb]').forEach(function(im){
im.onerror=function(){
const cv=document.createElement('canvas'); cv.width=128; cv.height=128; cv.className='mbase';
const ctx=cv.getContext('2d'),box=128;
rr(ctx,0,0,box,box,box*0.14,'#00b8b1');
rr(ctx,box*0.18,box*0.18,box*0.64,box*0.64,box*0.1,'#053b38');
ctx.strokeStyle='#7fe7e2';ctx.lineWidth=box*0.045;
ctx.beginPath();ctx.arc(box/2,box/2,box*0.19,0,7);ctx.stroke();
im.replaceWith(cv);
};
});
strip.querySelectorAll('.strow6').forEach(function(row,i){
row.onclick=function(){
const c=states[i];
setSetting('taskbarMode',c[0]);
setSetting('overlayBadges',c[1]?'on':'off');
showApplyProgress();
};
});
}
function rr(ctx,x,y,w,h,r,fill){ctx.fillStyle=fill;ctx.beginPath();if(ctx.roundRect)ctx.roundRect(x,y,w,h,r);else ctx.rect(x,y,w,h);ctx.fill();}
function drawNameTile(ctx,box,nm){
rr(ctx,0,0,box,box,0,'#00b8b1');
const FLOOR=box*0.36;
const wFor=function(i,total){return (total===2&&i===0)?box*0.80:box*0.92;};
const setF=function(sz){ctx.font='700 '+sz+"px 'Familjen Grotesk','Segoe UI',sans-serif";};
const wid=function(t,sz){setF(sz);return ctx.measureText(t).width;};
const fitSize=function(lines){let sz=box*0.9;
for(let i=0;i<90;i++){let okf=true;
for(let j=0;j<lines.length;j++){if(wid(lines[j],sz)>wFor(j,lines.length)){okf=false;break;}}
if(okf&&lines.length*sz*0.95<=box*0.88)break;sz*=0.95;}
return sz;};
const cands=[[nm]];const sp=nm.lastIndexOf(' '),hy=nm.indexOf('-');
if(sp>0)cands.push([nm.slice(0,sp),nm.slice(sp+1)]);else if(hy>0)cands.push([nm.slice(0,hy+1),nm.slice(hy+1)]);
if(sp<0&&hy<0&&nm.length>=7){const mm=Math.ceil(nm.length/2);cands.push([nm.slice(0,mm),nm.slice(mm)]);}
let best=[nm],bf=0;for(const c of cands){const f=fitSize(c);if(f>bf){bf=f;best=c;}}
if(bf<FLOOR){bf=FLOOR;
const take=function(str,maxw){let n=0;for(let i=1;i<=str.length;i++){if(wid(str.slice(0,i),FLOOR)<=maxw)n=i;else break;}return n;};
let rest=String(nm).trim();const out=[];
for(let ln=0;ln<2&&rest;ln++){
let n=take(rest,wFor(ln,2));
if(n>=rest.length){out.push(rest);rest='';break;}
if(n<1)n=1;
const seg=rest.slice(0,n+1);
const bp=Math.max(seg.lastIndexOf(' '),seg.lastIndexOf('-'));
const cut=(bp>=Math.ceil(n*0.5))?bp+(seg.charAt(bp)==='-'?1:0):n;
out.push(rest.slice(0,cut).trim());
rest=rest.slice(cut).replace(/^[ -]+/,'');}
best=out.filter(Boolean);}
setF(bf);ctx.fillStyle='#05221f';ctx.textAlign='left';ctx.textBaseline='alphabetic';
const lh=bf*0.95,mx=box*0.055,baseY=box-box*0.05-bf*0.14;
best.forEach(function(ln,i){ctx.fillText(ln,mx,baseY-(best.length-1-i)*lh);});
}
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(sc){sc.scrollTop=keepTop;}}
}
document.addEventListener('click',ev=>{
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){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;}
});
document.addEventListener('change',ev=>{
const c=ev.target.closest('[data-imchk]');
if(c){_importSel[c.dataset.imchk]=c.checked;}
});
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]');
if(sl){
const def=SETTINGS_DEF.find(d=>d.k===sl.dataset.sel);
if(def&&def.imgBy){const im=sl.closest('.sub').querySelector('.sexample');if(im)im.src='/dashboard/example?n='+def.imgBy[sl.value]+'&v='+PAGE_VERSION;}
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';}
b.addEventListener('mouseenter',()=>{clearTimeout(hide);show=setTimeout(()=>{place();pop.classList.add('show');},600);});
b.addEventListener('mouseleave',()=>{clearTimeout(show);hide=setTimeout(()=>pop.classList.remove('show'),220);});
pop.addEventListener('mouseenter',()=>clearTimeout(hide));
pop.addEventListener('mouseleave',()=>{hide=setTimeout(()=>pop.classList.remove('show'),220);});
})();
document.addEventListener('error',ev=>{const i=ev.target;if(i&&i.tagName==='IMG'&&i.dataset&&i.dataset.fallback==='hide')i.style.visibility='hidden';},true);
document.addEventListener('click',ev=>{
const tog=ev.target.closest('[data-toggle]');
if(tog){const id=tog.dataset.toggle;if(COLL.has(id))COLL.delete(id);else COLL.add(id);render();return;}
const jmp=ev.target.closest('[data-jump]');
if(jmp){const t=document.getElementById(jmp.dataset.jump);if(t){t.scrollIntoView({behavior:'smooth',block:'start'});t.style.transition='box-shadow .3s';t.style.boxShadow='0 0 0 2px #00b8b1';setTimeout(()=>t.style.boxShadow='',900);}return;}
const b=ev.target.closest('[data-act]');if(!b)return;
const a=b.dataset.act,c=b.dataset.confirm;if(c&&!confirm(c))return;
if(a==='flash-group'||a==='unflash-group'){(b.dataset.ids||'').split(',').filter(Boolean).forEach(sid=>act({action:a==='flash-group'?'flash':'unflash',sessionId:sid}));return;}
act({action:a,sessionId:b.dataset.sid,tabId:b.dataset.tid});
});
_q('#f').onchange=render; _q('#q').oninput=render;
const es=new EventSource('/dashboard/events');
es.onmessage=e=>{const d=JSON.parse(e.data);if(d.error){_q('#m').innerHTML='<div class="empty">snapshot error: '+esc(d.error)+'</div>';_q('#live').classList.add('stale');return;}SNAP=d;_q('#live').classList.remove('stale');render();};
es.onerror=()=>_q('#live').classList.add('stale');
/* ── the SHARED newbie panel (adom-ui-design 2.0.0) ───────────────────────────────────────────
pup owns exactly three things here: the phrases, the noun, and what the demo button does.
Everything else (card, grid, table, icons, clipboard, send with its version gate and
three-state verification, toasts, tooltips, the X and the header toggle) belongs to the module.
apiBase is left at its default so the two Hydrogen calls go SAME-ORIGIN and this bridge relays
them; see the /updater/status and /ai-threads/run-prompt routes in server.js. */
AdomNewbiePanel.mount({
mount: '#newbie',
app: 'pup',
appName: 'Pup',
slug: 'pup-bridge',
toggleButton: '#recall',
demo: {
label: 'Play the demo',
meta: '5 steps, about 2 minutes',
onPlay: function () {
AdomNewbiePanel.toast('Handing the demo script to a new AI thread. This can take up to two minutes.');
fetch('/api/demo-send', {method:'POST'})
.then(function(r){ return r.json(); })
.then(function(j){ AdomNewbiePanel.toast(j.ok ? 'The demo is running in a new AI thread.' : (j.why || 'Could not reach the AI panel.'), j.ok ? '' : 'err'); })
.catch(function(){ AdomNewbiePanel.toast('Could not reach the AI panel.', 'err'); });
}
},
phrases: ${JSON.stringify(SAY_PHRASES)}
});
/* Status pills, straight from /api/status so the pills and the LED feed cannot drift. */
async function paintLeds(){
var host=document.getElementById('leds'); if(!host) return;
try{
var j=await (await fetch('/api/status',{cache:'no-store'})).json();
host.innerHTML=(j.leds||[]).map(function(l){
var esc=function(t){ return String(t==null?'':t).replace(/&/g,'&').replace(/</g,'<'); };
return '<span class="led-pill" title="'+esc(l.detail)+'"><span class="d '+esc(l.state)+'"></span>'
+'<span class="lb">'+esc(l.label)+'</span><span class="sh">'+esc(l.short)+'</span></span>';
}).join('');
}catch(e){}
}
paintLeds(); setInterval(paintLeds,15000);
</script></body></html>