app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
← Commit history
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">…</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">×</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’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">⏮</button>- <button class="tbtn" id="tplay" data-tip="Pause or resume">⏸</button>- <button class="tbtn" id="tnext" data-tip="Next step">⏭</button>- <span class="tsep"></span>- <button class="tbtn" id="tmute" data-tip="Mute or unmute the narration">🔊</button>- <span class="tsep"></span>- <button class="tbtn texit" id="texit" data-tip="Exit the demo">✕</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,'"')+'" title="'+p.replace(/"/g,'"')+'">'- +'<td class="phrase">“'+q+'”</td>'+ return '<tr data-say="'+p.replace(/"/g,'"')+'">'+ +'<td class="phrase" title="'+p.replace(/"/g,'"')+'">“'+q+'”</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){}