app
adom-vscode
Public Made by Adomby adom
Let Claude, Codex and Kimi drive VS Code inside your Adom container
← Commit history
release 1.1.83
10 files changed
+160−27
VERSION+1−1@@ -1 +1 @@-1.1.82+1.1.83
bin/adom-vscode⋯ 1 unchanged line ⋯
bin/hydrogen-widget-claude-usage+3−3@@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Claude usage in the title bar: 5-hour session, weekly, and the weekly model buckets that apply to your plan, as percent used. Reads the Claude Code sign-in in ~/.claude/.credentials.json-and asks the same usage endpoint the Claude Code CLI shows in /usage. Asks the provider at most once every three minutes, pauses while the window is hidden, backs off on errors."""+and asks the same usage endpoint the Claude Code CLI shows in /usage. Asks the provider at most once every three minutes, pauses while the window is hidden, retries at 5, 15, 45 s on errors. Click the slot to cycle the views (rows, one number, a ring, a pair, all numbers, all rings).""" import json, os, sys, time sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) from importlib.machinery import SourceFileLoader@@ -42,7 +42,7 @@ def data(): for k, v in u.items(): if k.startswith('seven_day_') and isinstance(v, dict) and v.get('utilization') is not None: bars.append({'label': k[len('seven_day_'):].replace('_', ' '), 'pct': v['utilization'], 'severity': 'normal'})- return {'bars': bars}+ return {'items': bars} -RENDER = r"""(d.error?`<span class="err">${d.error}</span>`:(d.bars||[]).map(b=>bar(b.label,b.pct,b.severity)).join(''))"""+RENDER = r"""(d.error?`<span class="err">${d.error}</span>`:(d.items||[]).map(b=>bar(b.label,b.pct,b.severity)).join(''))""" lib.serve('Claude usage', data, RENDER, 180000, mark=marks.MARKS['claude'])
bin/hydrogen-widget-codex-usage+5−5@@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Codex usage in the title bar: the plan's rate-limit window as percent used, plus a secondary window when the plan has one. Reads the Codex sign-in in ~/.codex/auth.json and asks-the usage endpoint the Codex CLI shows in /status. Asks the provider at most once every three minutes, pauses while the window is hidden, backs off on errors."""+the usage endpoint the Codex CLI shows in /status. Asks the provider at most once every three minutes, pauses while the window is hidden, retries at 5, 15, 45 s on errors. Click the slot to cycle the views (rows, one number, a ring, the pair, both as numbers or rings).""" import json, os, sys from importlib.machinery import SourceFileLoader marks = SourceFileLoader('hwmarks', os.path.join(os.path.dirname(os.path.realpath(__file__)), 'hydrogen-widget-marks.py')).load_module()@@ -20,9 +20,9 @@ def data(): def label(w): secs = w.get('limit_window_seconds') or 0 return 'week' if secs >= 6*86400 else ('5h' if secs >= 4*3600 else f"{max(1, secs//3600)}h")- out = {'plan': u.get('plan_type'), 'primary': {'label': label(p), 'pct': p.get('used_percent', 0)}}- if s: out['secondary'] = {'label': label(s), 'pct': s.get('used_percent', 0)}- return out+ items = [{'label': label(p), 'pct': p.get('used_percent', 0) or 0, 'severity': 'normal'}]+ if s: items.append({'label': label(s), 'pct': s.get('used_percent', 0) or 0, 'severity': 'normal'})+ return {'plan': u.get('plan_type'), 'items': items} -RENDER = r"""(d.error?`<span class="err">${d.error}</span>`:bar(d.primary.label,d.primary.pct)+(d.secondary?bar(d.secondary.label,d.secondary.pct):''))"""+RENDER = r"""(d.error?`<span class="err">${d.error}</span>`:(d.items||[]).map(b=>bar(b.label,b.pct,b.severity)).join(''))""" lib.serve('Codex usage', data, RENDER, 180000, mark=marks.MARKS['codex'])
bin/hydrogen-widget-lib.py+126−14@@ -3,7 +3,20 @@ the title-bar slot and a JSON feed the page polls. A widget binary only needs: serve on $HYDROGEN_WIDGET_PORT, answer GET / with HTML sized for a ~30 px strip, and keep the JSON fresh. Hydrogen hands the slot over as soon as GET / answers (or the binary can POST-$ADOM_HD_CONTROL_URL/widgets/navigate {id, url} itself)."""+$ADOM_HD_CONTROL_URL/widgets/navigate {id, url} itself).++Views (John 2026-09-15, "the font size is too small"): a widget that returns `items`+(label, pct, severity) gets a set of layouts for free and the user clicks the slot to cycle+them: every meter as rows (the original), one item alone as a big number, one item as a ring+gauge, any pair side by side, all items as numbers, all items as rings. Each view asks the+Title Bar for exactly the width it needs, so a single number takes a third of the space the+rows did and its digits fill the slot's height. The chosen view is remembered on disk.++Robustness (John 2026-09-15, "HTTP Error 404" after a reboot): the last good numbers are+saved to disk and shown dimmed from the first second after a restart; a provider error is said+in plain words ("no answer from OpenAI (HTTP 404), retrying") and retried at 5, 15, 45 s+before backing off; the page polls again at the server's own retry time, never three minutes+later.""" import json, os, sys, time, threading, urllib.request from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer @@ -12,7 +25,7 @@ PAGE = """<!doctype html><meta charset="utf-8"><title>%(title)s</title> html,body{margin:0;height:100%%;background:transparent;color:#e6ebef;font-family:"Familjen Grotesk","Satoshi",system-ui,sans-serif;overflow:hidden} /* Two layouts, chosen by the slot's height (the Title Bar, full or compact), like the meter: tall = rows stacked to use the height; short = one horizontal row. */-.w{display:flex;align-items:center;height:100vh;padding:0 5px;gap:5px;white-space:nowrap}+.w{display:flex;align-items:center;height:100vh;padding:0 5px;gap:5px;white-space:nowrap;cursor:pointer;user-select:none} .mark{display:inline-flex;flex:none;width:15px;height:15px}.mark svg{width:100%%;height:100%%} .rows{display:flex;align-items:center;gap:8px;min-width:0;flex:1 1 auto} .row{display:flex;align-items:center;gap:4px}@@ -24,11 +37,23 @@ html,body{margin:0;height:100%%;background:transparent;color:#e6ebef;font-family .big{font-weight:600;font-variant-numeric:tabular-nums} .err{color:#f0a060} #w.stale{opacity:.45}+/* Views: a big number per item (cell), pairs with a hairline between (sep), ring gauges. */+.cell{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:1px;min-width:0}+.pct{font-weight:700;font-variant-numeric:tabular-nums;line-height:1;color:#19b3a6;letter-spacing:-.01em}+.pct.warn{color:#f0a060}.pct.hot{color:#e5484d}+.sep{flex:none;width:1px;align-self:stretch;background:rgba(255,255,255,.18);margin:0 2px}+.rw{display:flex;align-items:center;gap:4px}+.ring{position:relative;flex:none}.ring svg{width:100%%;height:100%%;transform:rotate(-90deg);display:block}+.ring .in{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-weight:700;font-variant-numeric:tabular-nums;line-height:1} @media (max-height:33px){ .w{font-size:clamp(7px,37vh,12px);line-height:1.2} .lbl{font-size:clamp(6px,30vh,9px)}.bar{width:clamp(24px,130vh,40px);height:clamp(4px,20vh,6px)}.v{min-width:2.4em} .big{font-size:clamp(9px,47vh,14px)}.err{font-size:clamp(7px,33vh,10px)} .mark{width:clamp(10px,55vh,17px);height:clamp(10px,55vh,17px)}+ .cell{flex-direction:row;align-items:baseline;gap:3px}+ .pct{font-size:clamp(9px,66vh,14px)}.cell .lbl{font-size:clamp(6px,32vh,8px)}+ .ring{width:clamp(10px,80vh,17px);height:clamp(10px,80vh,17px)}.ring .in{display:none}+ .nums .pct{font-size:clamp(9px,58vh,13px)} } @media (min-height:34px){ /* Tall: rows stacked. Columns take their natural width (label, value) and the bar takes the@@ -39,18 +64,58 @@ html,body{margin:0;height:100%%;background:transparent;color:#e6ebef;font-family .w{font-size:8px;line-height:1;padding:1px 4px;gap:4px} .mark{width:13px;height:13px} .rows{flex-direction:column;align-items:stretch;justify-content:center;gap:2px;min-width:0}+ .rows.hz{flex-direction:row;align-items:center;gap:6px} .row{display:grid;grid-template-columns:auto minmax(18px,1fr) auto;align-items:center;gap:3px;min-width:0} .lbl{font-size:6.5px}.row .lbl{max-width:4.2em;overflow:hidden;text-overflow:ellipsis}.bar{width:auto;height:4px}.v{font-size:8px;min-width:0} .big{font-size:14px}.err{font-size:8px;white-space:normal;line-height:1.1;max-width:96px}+ /* Big numbers fill the 36 px the slot gives: 22 px digits over a 7 px label. */+ .cell .lbl{font-size:7px}.pct{font-size:22px}+ .nums .pct{font-size:16px}.nums .cell{gap:2px}+ .ring{width:30px;height:30px}.ring .in{font-size:9.5px}+ .rw .cell .lbl{font-size:7px} } @media (max-height:33px){ .err{overflow:hidden;text-overflow:ellipsis;max-width:100%%} }-</style><body><div class="w">%(mark_html)s<div class="rows" id="w">%(title)s…</div></div>+</style><body><div class="w" id="wrap">%(mark_html)s<div class="rows" id="w">%(title)s…</div></div> <script> const R=%(refresh_ms)d;-function bar(l,p,sev){const c=(sev==='critical'||p>=90)?'hot':(sev==='warning'||p>=70)?'warn':'';return `<span class="row"><span class="lbl">${l}</span><span class="bar ${c}"><i style="width:${Math.max(0,Math.min(100,p))}%%"></i></span><span class="v">${Math.round(p)}%%</span></span>`;}+function sev(p,s){return (s==='critical'||p>=90)?'hot':(s==='warning'||p>=70)?'warn':'';}+function bar(l,p,s){const c=sev(p,s);return `<span class="row"><span class="lbl">${l}</span><span class="bar ${c}"><i style="width:${Math.max(0,Math.min(100,p))}%%"></i></span><span class="v">${Math.round(p)}%%</span></span>`;}+function cell(it){return `<span class="cell"><span class="lbl">${it.label}</span><span class="pct ${sev(it.pct,it.severity)}">${Math.round(it.pct)}%%</span></span>`;}+function ring(it,withLabel){const p=Math.max(0,Math.min(100,it.pct));const r=15.5,c=2*Math.PI*r;const col={hot:'#e5484d',warn:'#f0a060'}[sev(it.pct,it.severity)]||'#19b3a6';+ const g=`<span class="ring"><svg viewBox="0 0 36 36"><circle cx="18" cy="18" r="${r}" fill="none" stroke="rgba(255,255,255,.14)" stroke-width="4.5"/><circle cx="18" cy="18" r="${r}" fill="none" stroke="${col}" stroke-width="4.5" stroke-dasharray="${(p/100*c).toFixed(1)} ${c.toFixed(1)}" stroke-linecap="round"/></svg><span class="in">${Math.round(p)}</span></span>`;+ const compact=window.innerHeight<=33;+ const lab=compact?`<span class="cell"><span class="lbl">${it.label}</span><span class="pct ${sev(it.pct,it.severity)}">${Math.round(p)}%%</span></span>`:`<span class="lbl">${it.label}</span>`;+ return `<span class="rw">${g}${withLabel?lab:''}</span>`;}+/* The views a set of items offers, in click order. */+function viewsFor(items){const n=items.length;const v=[{id:'rows',name:'all meters'}];+ items.forEach((it,i)=>v.push({id:'big:'+i,name:it.label+' only'}));+ items.forEach((it,i)=>v.push({id:'ring:'+i,name:it.label+' as a ring'}));+ for(let i=0;i<n;i++)for(let j=i+1;j<n;j++)v.push({id:'duo:'+i+','+j,name:items[i].label+' + '+items[j].label});+ if(n>1){v.push({id:'nums',name:'all as numbers'});v.push({id:'rings',name:'all as rings'});}+ return v;}+let curView=(function(){try{return localStorage.getItem('hw.view')||'rows';}catch(e){return 'rows';}})();+function renderItems(items,id){const [kind,arg]=String(id).split(':');const at=(k)=>items[Math.max(0,Math.min(items.length-1,+k||0))];+ switch(kind){+ case 'big':return cell(at(arg));+ case 'ring':return ring(at(arg),true);+ case 'duo':{const [a,b]=String(arg||'0,1').split(',');return cell(at(a))+'<span class="sep"></span>'+cell(at(b));}+ case 'nums':return '<span class="nums rw">'+items.map(cell).join('<span class="sep"></span>')+'</span>';+ case 'rings':return '<span class="nums rw">'+items.map(it=>ring(it,true)).join('')+'</span>';+ default:return items.map(b=>bar(b.label,b.pct,b.severity)).join('');}} /* d.stale = the last good data while the provider is unreachable or the token is between refreshes: drawn dimmed, the reason in the tooltip, never an error where numbers were. */-function render(d0){const d=d0&&d0.stale?d0.stale:d0;const el=document.getElementById('w');el.classList.toggle('stale',!!(d0&&d0.stale));el.title=(d0&&d0.stale)?String(d0.error||''):'';el.innerHTML=%(render_js)s;postWidth();}+function render(d0){const d=d0&&d0.stale?d0.stale:d0;const el=document.getElementById('w');const wrap=document.getElementById('wrap');+ el.classList.toggle('stale',!!(d0&&d0.stale));let tip=(d0&&d0.stale)?String(d0.error||''):'';+ if(d0&&d0.view&&d0.view!==curView){curView=d0.view;try{localStorage.setItem('hw.view',curView);}catch(e){}}+ if(d&&Array.isArray(d.items)&&d.items.length){const vs=viewsFor(d.items);let k=vs.findIndex(v=>v.id===curView);if(k<0){k=0;curView='rows';}+ el.classList.toggle('hz',curView!=='rows');el.innerHTML=renderItems(d.items,curView);+ tip=(tip?tip+'. ':'')+'%(title)s: '+vs[k].name+'. Click for the next view ('+(k+1)+' of '+vs.length+').';}+ else{el.classList.remove('hz');el.innerHTML=%(render_js)s;}+ wrap.title=tip;postWidth();}+function setView(id){curView=id;try{localStorage.setItem('hw.view',id);}catch(e){}+ fetch('/view',{method:'POST',body:id}).catch(()=>{});if(window.__last)render(window.__last);}+document.getElementById('wrap').addEventListener('click',()=>{const d0=window.__last;const d=d0&&d0.stale?d0.stale:d0;if(!d||!Array.isArray(d.items)||!d.items.length)return;+ const vs=viewsFor(d.items);const k=vs.findIndex(v=>v.id===curView);setView(vs[(k+1)%%vs.length].id);}); /* THE WIDGET DECIDES ITS WIDTH: ask the host for the intrinsic width of what is shown in the current layout (tall rows are narrow, the compact row is wide). Measured on a hidden clone laid out at max-content, so the slot's current width never feeds back into the answer (rows stretch to@@ -62,8 +127,13 @@ function postWidth(){try{const w=document.querySelector('.w');if(!w)return; if(total>0&&window.parent!==window)window.parent.postMessage({hdWidget:{width:total}},'*');}catch(e){}} /* A failed poll (the widget process restarting, the proxy between the page and it) shows the last data dimmed and tries again in 5 s, then 15, then 45, then the normal interval: never "no data"- for three minutes over a blip (AdomLapper 2026-09-14). */-let fails=0;async function tick(){try{const r=await fetch('/data.json',{cache:'no-store'});if(!r.ok)throw new Error('http '+r.status);const d=await r.json();fails=0;window.__last=d;render(d);}catch(e){fails++;if(window.__last){render({error:'no answer from the widget',stale:window.__last});}else{document.getElementById('w').innerHTML='<span class="err">no data</span>';postWidth();}const back=[5000,15000,45000][fails-1];if(back)setTimeout(tick,back);}}+ for three minutes over a blip (AdomLapper 2026-09-14). When the server itself is waiting on the+ provider it says when it will try again (retry_in) and the page asks again right after. */+let fails=0,qt=null;function quick(ms){clearTimeout(qt);qt=setTimeout(tick,ms);}+async function tick(){try{const r=await fetch('/data.json',{cache:'no-store'});if(!r.ok)throw new Error('http '+r.status);const d=await r.json();fails=0;window.__last=d;render(d);+ if(d.error&&typeof d.retry_in==='number'&&d.retry_in*1000<R)quick(Math.max(2000,d.retry_in*1000+500));}+ catch(e){fails++;if(window.__last){render({error:'no answer from the widget, retrying',stale:window.__last.stale||window.__last});}else{render({error:'no answer from the widget, retrying'});}+ quick([5000,15000,45000][Math.min(fails-1,2)]);}} /* align_ms: fire on the boundary (a clock ticks once per minute, on the minute) instead of every R ms. */ const A=%(align_ms)d;let tm=null;function arm(){if(tm)return;if(A){const ms=A-(Date.now()%%A)+50;tm=setTimeout(()=>{tm=null;tick();arm();},ms);}else tm=setInterval(tick,R);}function disarm(){if(tm){if(A)clearTimeout(tm);else clearInterval(tm);tm=null;}} document.addEventListener('visibilitychange',()=>{if(document.hidden)disarm();else{tick();arm();}});@@ -73,12 +143,33 @@ tick();if(!document.hidden)arm(); let rz=null;window.addEventListener('resize',()=>{clearTimeout(rz);rz=setTimeout(()=>{if(window.__last)render(window.__last);},80);}); </script></body>""" +def _state_path(title):+ d = os.path.expanduser('~/.adom/widgets'); os.makedirs(d, exist_ok=True)+ return os.path.join(d, ''.join(ch if ch.isalnum() else '-' for ch in title.lower()) + '.state.json')++def _load_state(title):+ try: return json.load(open(_state_path(title)))+ except Exception: return {}++def _save_state(title, st):+ try:+ p = _state_path(title); tmp = p + '.tmp'+ json.dump(st, open(tmp, 'w')); os.replace(tmp, p)+ except Exception: pass+ def serve(title, data_fn, render_js, refresh_ms=60000, mark=None, align_ms=0): mark = mark or {'svg': '', 'color': 'currentColor'} port = int(os.environ.get('HYDROGEN_WIDGET_PORT') or (sys.argv[sys.argv.index('--port')+1] if '--port' in sys.argv else 8930))- # One upstream call per interval at most, however often the page asks; on an upstream- # error the interval doubles up to 15 minutes and returns to normal on the next success.- cache = {'t': 0, 'v': {'error': 'starting'}, 'wait': refresh_ms/1000, 'good': None}+ provider = 'Anthropic' if 'Claude' in title else ('OpenAI' if 'Codex' in title else 'the provider')+ # One upstream call per interval at most, however often the page asks. On an upstream error+ # the next tries come at 5, 15, 45, 90 s, then minutes, capped at 15; back to normal on the+ # next success. The last good numbers live on disk so a restart shows them at once, dimmed.+ st = _load_state(title)+ cache = {'t': 0, 'v': {'error': 'starting'}, 'wait': refresh_ms/1000, 'good': None, 'fails': 0, 'view': str(st.get('view') or 'rows')}+ if isinstance(st.get('good'), dict) and st['good'].get('items') is not None:+ cache['good'] = st['good']+ when = time.strftime('%H:%M', time.localtime(st.get('at') or 0)) if st.get('at') else 'earlier'+ cache['v'] = {'error': 'waiting for %s, last numbers from %s' % (provider, when), 'stale': cache['good']} lock = threading.Lock() def _creds_mtime(): try: return max(os.stat(os.path.expanduser(f)).st_mtime for f in ('~/.claude/.credentials.json', '~/.codex/auth.json') if os.path.exists(os.path.expanduser(f)))@@ -92,7 +183,9 @@ def serve(title, data_fn, render_js, refresh_ms=60000, mark=None, align_ms=0): cache['expired_mtime'] = None try: cache['v'] = data_fn(); cache['wait'] = refresh_ms/1000- if not cache['v'].get('error'): cache['good'] = cache['v']+ if not cache['v'].get('error'):+ cache['good'] = cache['v']; cache['fails'] = 0+ _save_state(title, {'good': cache['good'], 'at': time.time(), 'view': cache['view']}) except Exception as e: # A 401/403 is almost always an EXPIRED access token, not a missing sign-in: # Claude Code and Codex refresh their own tokens the next time they run, and@@ -108,15 +201,34 @@ def serve(title, data_fn, render_js, refresh_ms=60000, mark=None, align_ms=0): # fifteen minutes as a safety net. A stat per page poll is the whole cost. cache['wait'] = 900; cache['expired_mtime'] = _creds_mtime() else:- msg = str(e)[:120]; cache['wait'] = min(900, max(refresh_ms/1000, cache['wait']) * 2)+ # Plain words, never a raw exception (AdomLapper 2026-09-15 after a reboot:+ # "HTTP Error 404: Not Found" sat in the Title Bar for fifteen minutes while+ # the endpoint answered 200 to a manual call a minute later).+ reason = ('HTTP %s' % code) if code else ('network' if 'URLError' in type(e).__name__ or 'timed out' in str(e) else type(e).__name__)+ msg = 'no answer from %s (%s), retrying' % (provider, reason)+ cache['fails'] += 1+ cache['wait'] = [5, 15, 45, 90, 180, 300, 600, 900][min(cache['fails'] - 1, 7)] cache['v'] = {'error': msg, 'stale': cache['good']} if cache['good'] else {'error': msg} cache['t'] = time.time()- return cache['v']+ out = dict(cache['v']); out['view'] = cache['view']+ if out.get('error'): out['retry_in'] = max(0, int(cache['wait'] - (time.time() - cache['t'])))+ return out+ def set_view(v):+ with lock:+ cache['view'] = v+ s = _load_state(title); s['view'] = v; _save_state(title, s) class H(BaseHTTPRequestHandler): def log_message(self, *a): pass def _send(self, code, ctype, body): self.send_response(code); self.send_header('content-type', ctype); self.send_header('cache-control', 'no-store') self.send_header('access-control-allow-origin', '*'); self.end_headers(); self.wfile.write(body)+ def do_POST(self):+ if self.path.startswith('/view'):+ n = int(self.headers.get('content-length') or 0)+ v = self.rfile.read(n).decode(errors='replace').strip()[:40] if n else ''+ if v: set_view(v)+ self._send(200, 'application/json', json.dumps({'ok': True, 'view': cache['view']}).encode())+ else: self._send(404, 'text/plain', b'not found') def do_GET(self): if self.path.startswith('/data.json'): self._send(200, 'application/json', json.dumps(data()).encode()) elif self.path.startswith('/preview'):@@ -137,7 +249,7 @@ def serve(title, data_fn, render_js, refresh_ms=60000, mark=None, align_ms=0): 'const f=document.getElementById("w");f.dataset.w=m.width;if(!fixed)f.style.width=Math.max(48,Math.min(640,m.width))+"px";});</script></body>') % (title, w, h, 'true' if fixed else 'false') self._send(200, 'text/html; charset=utf-8', page.encode()) elif self.path.startswith('/health'): self._send(200, 'application/json', b'{"ok":true}')- else: self._send(200, 'text/html; charset=utf-8', (PAGE % {'title': title, 'refresh_ms': refresh_ms, 'align_ms': int(align_ms), 'render_js': render_js, 'mark_html': ('<span class="mark" style="color:%s"><svg viewBox="0 0 16 16">%s</svg></span>' % (mark['color'], mark['svg'])) if (mark and mark.get('svg')) else ''}).encode())+ else: self._send(200, 'text/html; charset=utf-8', (PAGE % {'title': title, 'refresh_ms': refresh_ms, 'align_ms': int(align_ms), 'render_js': render_js, 'mark_html': ('<span class="mark" style="color:%s"><svg viewBox="0 0 16 16">%s</svg></span>' % (mark.get('color', 'currentColor'), mark.get('svg', ''))) if mark.get('svg') else ''}).encode()) # Bind with retries: a previous instance Hydrogen just stopped may still be releasing the # port for a moment. Ten tries over five seconds, then give up loudly (the log says why). ThreadingHTTPServer.allow_reuse_address = True
bin/hydrogen-widget-marks.pyinstall.shpackage.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-vscode",- "version": "1.1.82",+ "version": "1.1.83", "type": "app", "description": "CLI + VS Code extension that lets AI assistants and the Hydrogen Desktop frontend drive VS Code from inside an Adom container: open files, live get/set of any setting/theme/font (no reload), native streamed exec, a frontend event queue, container identity (hd/hw), api-key status/inject, extension query/update, modes, paste screenshots, and any VS Code command via a local HTTP API on port 8821 (with port-file fallback discovery).", "license": "PROPRIETARY",
page.json+2−2@@ -4,7 +4,7 @@ "slug": "adom-vscode", "title": "adom-vscode", "brief": "Let Claude, Codex and Kimi drive VS Code inside your Adom container",- "version": "1.1.82",+ "version": "1.1.83", "license": "PROPRIETARY", "visibility": "public", "hero": {@@ -141,4 +141,4 @@ "adom-vscode-extensions", "adom-vscode-modes" ]-}+}
widgets/README.md+22−1@@ -20,9 +20,30 @@ Where the numbers come from: the Claude Code sign-in in your workspace (`~/.clau Your plan's rate-limit window as percent used, plus the secondary window when your plan has one. From the Codex sign-in in your workspace (`~/.codex/auth.json`), asked at `chatgpt.com/backend-api/wham/usage`, what Codex's `/status` reads. With no Codex sign-in the slot says so. +## Views: click the widget to change what it shows++The rows are the default, and they are small. Click the slot and it moves to the next view; the widget asks the Title Bar for exactly the width that view needs, so a single number takes about a third of the space the rows did and its digits fill the slot's height. The choice is remembered per widget, across restarts. Hover the slot for the name of the current view and how many there are.++For Claude usage (three meters: 5h, week, Fable) the views, in click order:++| view | what it shows |+|---|---|+| all meters | the three rows with bars (the default) |+| 5h only, week only, Fable only | one label over one big percent |+| 5h ring, week ring, Fable ring | one ring gauge with the percent inside, the label beside it |+| 5h + week, 5h + Fable, week + Fable | two big percents with a hairline between |+| all as numbers | the three percents in a row, no bars |+| all as rings | the three rings in a row |++Codex usage has the same set for its two windows (eight views). In the compact Title Bar the same views lay out in one line, label beside the number. The percent takes the provider's severity colour (teal, amber from 70, red from 90).++## After a restart, and when the provider does not answer++The last good numbers are kept on disk, so after a reboot the widget shows them at once, dimmed, with "waiting for Anthropic, last numbers from 08:41" in its tooltip, and replaces them the moment the provider answers. A provider error is said in plain words ("no answer from OpenAI (HTTP 404), retrying"), the numbers stay on screen dimmed, and the next try comes at 5, 15, 45 and 90 s, then minutes, capped at fifteen, back to normal on the first success. An expired token is different: the widget waits for Claude Code or Codex to refresh it and re-reads the sign-in file when it changes.+ ## How often they ask -At most once every three minutes per provider, whatever the page polls. The page pauses while the Hydrogen window is hidden or minimized and reads once on return. An upstream error doubles the interval up to 15 minutes, then recovers. The sign-in file is re-read only when it changes.+At most once every three minutes per provider, whatever the page polls. The page pauses while the Hydrogen window is hidden or minimized and reads once on return. An upstream error is retried at 5, 15, 45 and 90 s, then every few minutes (capped at fifteen), and the interval returns to normal on the next success. No model runs; asking for usage spends no quota. ## Two widgets from one repo: how that works