← Commit history

Publish 2.0.333

John Lauer ·21e0733a07 ·1mo ago ·parent 196595d
3 files changed +132−50
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "pup-bridge",-  "version": "2.0.332",+  "version": "2.0.333",   "type": "app",   "description": "The pup SKILL PACK \u2014 the AI skills that teach a cloud/container AI how to drive the Adom Desktop Puppeteer (pup) bridge (browser_* verbs): the everyday `pup` skill plus focused sub-skills for windows/sessions/tabs, screenshots+recording, browsers+Chrome install, and driving the Adom wiki. Installs into ~/.claude/skills + ~/.codex/skills. DOCS ONLY \u2014 this is NOT the bridge runtime. The bridge itself (the Node program that drives the browser) runs on the DESKTOP: Adom Desktop bundles it and auto-installs/updates it, and it's published separately on the Releases tab as adom-bridge-puppeteer-v<ver>.zip. The pkg version is kept in lockstep with the Release version so the page's max version row always carries BOTH a pkg tarball (skills) and a runtime zip (bridge) \u2014 a skills-only or runtime-only bump on the top row breaks constraint installs. TL;DR: pkg install = the skills for your AI; the bridge = the Release zip, which Adom Desktop manages for you.",   "keywords": [
page.json+1−1
@@ -4,7 +4,7 @@   "slug": "pup-bridge",   "title": "Pup - Puppeteer Bridge",   "brief": "pup is the AI's own browser: a real, full Chrome on the user's desktop that the AI fully controls (a sandbox, not the user's signed-in browser). Rides Bridge; pup_* verbs open windows and tabs, navigate, screenshot, and eval JS.",-  "version": "2.0.332",+  "version": "2.0.333",   "tags": [     "skills",     "pup",
tools/pup-dashboard+130−48
@@ -73,13 +73,48 @@ function censusThread(j) { // tagged dock:pup-bridge, and SUBMIT the phrase. New threads ONLY; never resume/target an existing // tid. On any failure the page falls back to copying, so the user is never stranded. async function sendToNewThread(phrase) {-  // EMERGENCY PARK (dock thread, 2026-08-24): the fleet Send-to-AI success path CRASHED the ah dev-  // shell on ConfRoomROG within ~30s of the run-prompt call. Until the main ah build thread-  // certifies the /ai-threads/new + run-prompt flow, pup's backend does NOT touch the control API at-  // all. It answers parked; the page's failure path (error toast + copy fallback) carries the user,-  // so Send to AI still copies the phrase. Un-park when the broadcast says the flow is certified.-  if (!SAY.includes(phrase)) return { ok: false, parked: true, why: 'unknown phrase' };-  return { ok: false, parked: true, why: 'Send to AI is temporarily parked fleet-wide (a control-API crash is under triage); the phrase was copied to your clipboard instead.' };+  // UN-PARKED 2026-08-24 (John: "we are re-enabling the send to ai"). The ah build thread certified+  // the crash family dead on the gate-fixed build (33ac2074, six raw-CDP typing helpers that ran+  // outside the concurrency gate), ~20 min of the kill condition with zero process deaths, and the+  // dock thread verified the CODE MARKER in the installed exe rather than trusting a version stamp.+  // Contract (say-strip): open a NEW thread named for the app, tagged dock:<WIKI PAGE SLUG>, submit+  // the phrase. NEW threads only: never resume, never target a tid from the census. On any failure+  // the page copies the phrase and toasts honestly, so the user is never stranded.+  if (!SAY.includes(phrase)) return { ok: false, why: 'unknown phrase' };+  // The fix shipped after published Hydrogen 1.0.141, so refuse HONESTLY on a pre-fix shell instead+  // of driving it into the crash we just cleared. 1.0.142+ (or a build reporting the marker) passes.+  const guard = await shellSupportsSend();+  if (!guard.ok) return { ok: false, why: guard.why };+  const mk = await hpost('/ai-threads/new', { name: 'Pup - say it', tags: ['dock:pup-bridge'] });+  if (!(mk.status >= 200 && mk.status < 300)) return { ok: false, why: 'Could not reach the AI panel (' + (mk.status || mk.error) + ')' };+  const sid = mk.json && (mk.json.sid || mk.json.id);+  if (!sid) return { ok: false, why: 'AI panel did not return a thread id' };+  const run = await hpost('/ai-threads/run-prompt', { sid, prompt: phrase, submit: true });+  if (run.status >= 200 && run.status < 300 && !(run.json && run.json.ok === false)) return { ok: true, sid };+  return { ok: false, why: 'Thread opened but the prompt was refused (' + (run.status || run.error) + ')' };+}++// Pre-fix shells carry the crash this feature had parked for it, so refuse with a next step rather+// than risking the user's editor. NOTE (measured 2026-08-24): Hydrogen's control API exposes NO+// semver. GET /version returns an endpoint list; GET /health returns build_sha / build_epoch /+// build_dirty. The build thread also verified the fix by CODE MARKER because the version stamp lies+// on incremental builds. So the gate is build_epoch: the certified gate-fixed build on AdomLapper+// reports 1787569730 (Aug 24 06:08), and stock 1.0.141 predates it. Anything at or after that epoch+// passes; anything older gets the honest refusal; an unreadable epoch FAILS OPEN (never block on a+// field we cannot read) and is logged so it can be corrected rather than silently trusted.+const SEND_FIX_EPOCH = 1787569730;+let _shellGate = { at: 0, ok: true, why: '' };+async function shellSupportsSend() {+  if (Date.now() - _shellGate.at < 60000) return _shellGate;+  const r = await hget('/health');+  const b = (r && r.json) || null;+  let out;+  if (!r.status || !b) out = { ok: false, why: 'Could not reach Hydrogen to check its build; the phrase was copied instead.' };+  else if (typeof b.build_epoch !== 'number') out = { ok: true, why: '' };   // fail open on a field we cannot read+  else if (b.build_epoch < SEND_FIX_EPOCH) out = { ok: false, why: 'Your Hydrogen predates the Send to AI fix (build ' + (b.build_local || b.build_epoch) + '). Update to 1.0.142 or newer to enable it; the phrase was copied instead.' };+  else out = { ok: true, why: '' };+  _shellGate = { at: Date.now(), ...out };+  return _shellGate; }  let TOOL_VERSION = '0';@@ -307,11 +342,10 @@ table.prompts td.phrase{font-size:14px;color:var(--text);white-space:nowrap;over table.prompts td.phrase em{color:var(--teal);font-style:normal} table.prompts td.acts{width:72px;text-align:right;white-space:nowrap} table.prompts td.acts .ibtn{display:inline-flex;vertical-align:middle;margin-left:6px}-.nb-collapsed{display:none;font-size:13px;color:var(--muted);cursor:pointer;padding:10px 0}-.nb-collapsed:hover{color:var(--teal)}-.newbie.folded .nb-cols,.newbie.folded h2,.newbie.folded .expl,.newbie.folded .nb-x{display:none}-.newbie.folded .nb-collapsed{display:block}-.newbie.folded{padding:6px 20px}+.newbie.closed{display:none}+/* 1.13.0: the panel FULLY closes (no residual one-liner) and the app UI moves up to fill the space;+   the header button is the only way back, and it is a real toggle. */+.ibtn[aria-pressed=true]{color:var(--teal);background:rgba(0,184,177,.10);border-color:var(--line);box-shadow:inset 0 1px 2px rgba(0,0,0,.35)} /* ── status pills ── */ .leds{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:20px} .led-pill{display:inline-flex;align-items:center;gap:7px;background:var(--surface);border:1px solid var(--line);border-radius:999px;padding:4px 12px;font-size:12.5px;cursor:pointer;position:relative}@@ -321,13 +355,20 @@ table.prompts td.acts .ibtn{display:inline-flex;vertical-align:middle;margin-lef .led-detail{position:absolute;top:28px;left:0;min-width:240px;max-width:360px;background:var(--surface);border:1px solid var(--line);border-radius:8px;padding:10px 12px;font-size:12.5px;color:var(--body);box-shadow:0 8px 24px rgba(0,0,0,.5);z-index:9;white-space:normal;text-align:left} .led-detail .fix{color:var(--teal);margin-top:6px} /* ── demo transport ── */-.transport{margin:0 0 20px;padding:12px;border:1px solid var(--line);border-radius:10px;background:var(--surface)}-.tstep{font:13px ui-monospace,monospace;color:var(--body);margin-bottom:8px}.tstep #tstepn{color:var(--teal);font-weight:600}.tstep #tstate{color:var(--muted);margin-left:8px}-.tbtns{display:flex;gap:8px;margin-bottom:10px;align-items:center}-.tbtn{background:var(--surface2);color:var(--text);border:1px solid var(--line);border-radius:7px;padding:6px 14px;font-size:16px;line-height:1;cursor:pointer}-.tbtn:hover{border-color:var(--teal);color:var(--teal)}.tbtn:active{transform:translateY(1px)}-.tbtn.texit{border-color:rgba(248,81,73,.5);color:var(--led-red)}.tbtn.texit:hover{background:rgba(248,81,73,.1)}-.tsep{width:1px;height:20px;background:var(--line);margin:0 2px}+/* 1.13.0: the demo toolbar lives INSIDE the panel, under the Play button, never floating bottom-centre.+   Body face throughout (NO monospace anywhere in the demo UI); tabular-nums so 3/11 does not jitter. */+.transport{margin-top:12px}+.tstep{font:12.5px var(--sans);font-variant-numeric:tabular-nums;color:var(--body);margin-bottom:8px}+.tstep #tstepn{color:var(--teal);font-weight:600;font-variant-numeric:tabular-nums}+.tstep #tsteptitle{color:var(--body)}.tstep #tstate{color:var(--muted);margin-left:6px}+.tbtns{display:flex;gap:6px;margin-bottom:10px;align-items:center}+.tbtn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;background:none;border:1px solid transparent;border-radius:6px;color:var(--muted);cursor:pointer;padding:0}+.tbtn svg{width:16px;height:16px}+.tbtn:hover{color:var(--teal);border-color:var(--line);background:rgba(0,184,177,.08)}+.tbtn:active{transform:translateY(1px)}+.tbtn:focus-visible{outline:2px solid var(--teal);outline-offset:1px}+.tbtn.texit:hover{color:var(--led-red);border-color:rgba(248,81,73,.5);background:rgba(248,81,73,.1)}+.tsep{width:1px;height:18px;background:var(--line);margin:0 3px} .tbar{height:6px;background:var(--surface2);border-radius:3px;overflow:hidden}.tfill{height:100%;width:0;background:var(--teal);transition:width .3s ease} /* ── tooltip + toast ── */ #tip{position:fixed;background:var(--surface2);border:1px solid var(--line);border-radius:6px;padding:5px 9px;font-size:12px;color:var(--body);pointer-events:none;z-index:99;box-shadow:0 6px 18px rgba(0,0,0,.5)}@@ -347,7 +388,7 @@ table.prompts td.acts .ibtn{display:inline-flex;vertical-align:middle;margin-lef   </div>   <div class="right">     <span class="chip" id="target" hidden><span class="dot"></span><span id="targetbox">&hellip;</span></span>-    <button class="ibtn" id="recall" aria-label="Show the getting-started panel" data-tip="Show the getting-started panel">+    <button class="ibtn" id="recall" aria-pressed="true" aria-label="Hide the prompts panel" data-tip="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="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>     </button>     <button class="ibtn" id="gear" aria-label="Settings" data-tip="Settings">@@ -358,7 +399,6 @@ table.prompts td.acts .ibtn{display:inline-flex;vertical-align:middle;margin-lef <div class="wrap">   <section class="newbie" id="newbie">     <button class="nb-x" id="nbx" aria-label="Collapse this panel" data-tip="Collapse this panel">&times;</button>-    <div class="nb-collapsed" id="nbc">Say it: <span id="nbcn">6</span> phrases</div>     <h2>Say it</h2>     <p class="expl">You never need this page&rsquo;s buttons: just tell your AI what you want, using the word <b class="noun">pup</b>. Copy a phrase, or send it straight to a new AI thread.</p>     <div class="nb-cols">@@ -368,24 +408,24 @@ table.prompts td.acts .ibtn{display:inline-flex;vertical-align:middle;margin-lef           <span id="demolbl">Play the demo</span>         </button>         <div class="play-sub" id="demosub">11 steps, about 2 minutes</div>+        <div id="transport" class="transport" hidden>+          <div class="tstep"><span id="tstepn">1/11</span> <span id="tsteptitle">Intro</span> <span id="tstate"></span></div>+          <div class="tbtns">+            <button class="tbtn" id="tprev" aria-label="Previous step" data-tip="Previous step"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M18.5 5.5v13L9 12z"/><path d="M6 5.5v13"/></svg></button>+            <button class="tbtn" id="tplay" aria-label="Pause" data-tip="Pause"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5.5v13"/><path d="M15 5.5v13"/></svg></button>+            <button class="tbtn" id="tnext" aria-label="Next step" data-tip="Next step"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5.5 5.5v13L15 12z"/><path d="M18 5.5v13"/></svg></button>+            <span class="tsep"></span>+            <button class="tbtn" id="tmute" aria-label="Mute the narration" data-tip="Mute the narration"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9.5v5h3.5L12 18.5v-13L7.5 9.5z"/><path d="M16 9.8a3 3 0 0 1 0 4.4"/><path d="M18.6 7.4a6.5 6.5 0 0 1 0 9.2"/></svg></button>+            <span class="tsep"></span>+            <button class="tbtn texit" id="texit" aria-label="Exit the demo" data-tip="Exit the demo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>+          </div>+          <div class="tbar"><div id="tfill" class="tfill"></div></div>+        </div>       </div>       <div class="nb-right"><table class="prompts"><tbody id="prompts"></tbody></table></div>     </div>   </section>   <div class="leds" id="leds"></div>-  <div id="transport" class="transport" hidden>-    <div class="tstep"><span id="tstepn">1/11</span> <span id="tsteptitle">Intro</span> <span id="tstate"></span></div>-    <div class="tbtns">-      <button class="tbtn" id="tprev" data-tip="Previous step">&#9198;</button>-      <button class="tbtn" id="tplay" data-tip="Pause or resume">&#9208;</button>-      <button class="tbtn" id="tnext" data-tip="Next step">&#9197;</button>-      <span class="tsep"></span>-      <button class="tbtn" id="tmute" data-tip="Mute or unmute the narration">&#128266;</button>-      <span class="tsep"></span>-      <button class="tbtn texit" id="texit" data-tip="Exit the demo">&#10005;</button>-    </div>-    <div class="tbar"><div id="tfill" class="tfill"></div></div>-  </div> </div> <div id="toast"></div> <script>@@ -409,11 +449,10 @@ function copyPhrase(text,quiet){var done=function(){if(!quiet)toast('success','C /* ── prompts TABLE ── */ (function(){   var tb=document.getElementById('prompts');-  document.getElementById('nbcn').textContent=SAY.length;   tb.innerHTML=SAY.map(function(p){     var q=p.replace(/\\bpup\\b/,'<em>pup</em>');-    return '<tr data-say="'+p.replace(/"/g,'&quot;')+'" title="'+p.replace(/"/g,'&quot;')+'">'-      +'<td class="phrase">&ldquo;'+q+'&rdquo;</td>'+    return '<tr data-say="'+p.replace(/"/g,'&quot;')+'">'+      +'<td class="phrase" title="'+p.replace(/"/g,'&quot;')+'">&ldquo;'+q+'&rdquo;</td>'       +'<td class="acts">'       +'<button class="ibtn act-copy" aria-label="Copy this prompt to your clipboard" data-tip="Copy this prompt to your clipboard">'+ICON_COPY+'</button>'       +'<button class="ibtn act-send" aria-label="Send this prompt to a new AI thread" data-tip="Send this prompt to a new AI thread">'+ICON_SEND+'</button>'@@ -433,16 +472,43 @@ function copyPhrase(text,quiet){var done=function(){if(!quiet)toast('success','C     copyPhrase(phrase);   /* the row itself, and the copy icon, both copy */   }); })();-/* ── panel fold / recall (newbie-graduation, slug-namespaced key) ── */+/* ── panel open/closed: ONE state, driven by both the X and the header toggle (1.13.0) ──+   The X removes the panel entirely (no residual one-liner) and the app UI moves up. The header+   button is a real toggle: aria-pressed, a teal glyph + inset background when open, muted when+   closed, and a label that swaps. The two controls can never drift because they call one setter.+   Graduation (newbie-graduation): the panel also folds after 3 successful launches or the first+   time the bridge sees the user's AI drive pup. The localStorage key is SLUG-NAMESPACED because a+   proxied dashboard shares one origin with every other app (measured: six other apps' keys sat+   beside mine), so the namespace is what provides isolation, not the origin. */+var NB_KEY='pup-bridge:newbieClosed', NB_LAUNCH='pup-bridge:launches', NB_GRAD='pup-bridge:graduated';+function setPanel(open){+  var panel=document.getElementById('newbie'), btn=document.getElementById('recall');+  panel.classList.toggle('closed',!open);+  btn.setAttribute('aria-pressed',String(open));+  var lbl=open?'Hide the prompts panel':'Show the prompts panel';+  btn.setAttribute('aria-label',lbl); btn.setAttribute('data-tip',lbl);+  try{ open?localStorage.removeItem(NB_KEY):localStorage.setItem(NB_KEY,'1'); }catch(e){}+} (function(){-  var panel=document.getElementById('newbie'), KEY='pup-bridge:newbieFolded';-  function fold(v){panel.classList.toggle('folded',v);try{v?localStorage.setItem(KEY,'1'):localStorage.removeItem(KEY);}catch(e){}}-  try{ if(localStorage.getItem(KEY)==='1') panel.classList.add('folded'); }catch(e){}-  document.getElementById('nbx').onclick=function(){fold(true);};-  document.getElementById('nbc').onclick=function(){panel.classList.remove('folded');};-  document.getElementById('recall').onclick=function(){fold(false);};-  document.getElementById('gear').onclick=function(){fetch('api/open-settings',{method:'POST'}).then(function(){toast('','Opening pup settings on your desktop\\u2026');}).catch(function(){});};+  var closed=false; try{ closed=localStorage.getItem(NB_KEY)==='1'; }catch(e){}+  setPanel(!closed);+  document.getElementById('nbx').onclick=function(){setPanel(false);};+  document.getElementById('recall').onclick=function(){+    setPanel(document.getElementById('newbie').classList.contains('closed'));+  };+  document.getElementById('gear').onclick=function(){fetch('api/open-settings',{method:'POST'}).then(function(){toast('','Opening pup settings on your desktop…');}).catch(function(){});}; })();+/* graduation triggers: 3 green launches, or the first AI-driven action pup reports */+function graduate(why){+  try{ if(localStorage.getItem(NB_GRAD)==='1') return; localStorage.setItem(NB_GRAD,'1'); }catch(e){}+  setPanel(false);+  toast('','Prompts panel tucked away ('+why+'). The ? in the header brings it back any time.');+}+function noteLaunch(state){+  if(state!=='ok') return;+  var n=0; try{ n=(parseInt(localStorage.getItem(NB_LAUNCH),10)||0)+1; localStorage.setItem(NB_LAUNCH,String(n)); }catch(e){}+  if(n>=3) graduate('you have launched pup '+n+' times');+} /* ── status ── */ function ledState(s){return s==='ok'?'ok':(s==='fail'||s==='bad'?'bad':(s==='warn'||s==='busy'?'busy':'unknown'));} async function load(){@@ -453,6 +519,14 @@ async function load(){     tgt.hidden=false;     document.getElementById('targetbox').innerHTML='target: <b>'+(d.desktop||'this machine')+'</b>';     tgt.setAttribute('data-state',ledState(d.state));+    if(!window.__pupCounted){ window.__pupCounted=true; noteLaunch(d.state); }+    // "the bridge saw the user's AI drive the app": a window owned by an AI thread other than the+    // dashboard's own demo session is the honest signal that they are past needing the prompts.+    try{+      var w=(d.leds||[]).find(function(l){return l.key==='windows';});+      var driven=(w&&w.sessions||[]).some(function(s2){return s2.owner && s2.sessionId!=='pup-dock-demo';});+      if(driven) graduate('your AI is driving pup now');+    }catch(e){}     var leds=d.leds||[];     document.getElementById('leds').innerHTML=leds.map(function(l,i){       var st=ledState(l.state);@@ -484,8 +558,12 @@ async function load(){   var paused=false;   document.getElementById('tplay').onclick=function(){paused=!paused;ctl(paused?'pause':'resume');};   var muted=false; try{muted=localStorage.getItem('pup-bridge:demoMuted')==='1';}catch(e){}-  var mb=document.getElementById('tmute'); mb.innerHTML=muted?'\\uD83D\\uDD07':'\\uD83D\\uDD0A';-  mb.onclick=function(){muted=!muted;try{localStorage.setItem('pup-bridge:demoMuted',muted?'1':'0');}catch(e){}mb.innerHTML=muted?'\\uD83D\\uDD07':'\\uD83D\\uDD0A';ctl(muted?'mute':'unmute');};+  var SPK='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9.5v5h3.5L12 18.5v-13L7.5 9.5z"/><path d="M16 9.8a3 3 0 0 1 0 4.4"/><path d="M18.6 7.4a6.5 6.5 0 0 1 0 9.2"/></svg>';+  var MUTED='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9.5v5h3.5L12 18.5v-13L7.5 9.5z"/><path d="m16.5 9.5 5 5"/><path d="m21.5 9.5-5 5"/></svg>';+  var mb=document.getElementById('tmute');+  function paintMute(){ mb.innerHTML=muted?MUTED:SPK; var l=muted?'Unmute the narration':'Mute the narration'; mb.setAttribute('aria-label',l); mb.setAttribute('data-tip',l); }+  paintMute();+  mb.onclick=function(){muted=!muted;try{localStorage.setItem('pup-bridge:demoMuted',muted?'1':'0');}catch(e){}paintMute();ctl(muted?'mute':'unmute');};   var poll=null;   function stopPoll(){if(poll){clearInterval(poll);poll=null;}tp.hidden=true;demoBtn.disabled=false;lbl.textContent='Play the demo';sub.textContent='11 steps, about 2 minutes';}   async function tick(){@@ -497,7 +575,11 @@ async function load(){       document.getElementById('tstepn').textContent=(d.idx+1)+'/'+d.total;       document.getElementById('tsteptitle').textContent=d.title||'';       paused=!!d.paused;-      document.getElementById('tplay').innerHTML=paused?'\\u25B6':'\\u23F8';+      var pb=document.getElementById('tplay');+      pb.innerHTML=paused+        ? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4.8v14.4a.6.6 0 0 0 .92.5l11.3-7.2a.6.6 0 0 0 0-1l-11.3-7.2a.6.6 0 0 0-.92.5z"/></svg>'+        : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5.5v13"/><path d="M15 5.5v13"/></svg>';+      var pl=paused?'Resume':'Pause'; pb.setAttribute('aria-label',pl); pb.setAttribute('data-tip',pl);       document.getElementById('tstate').textContent=paused?'(paused)':'';       document.getElementById('tfill').style.width=Math.round(100*(d.idx+1)/(d.total||1))+'%';     }catch(e){}