← Commit history
README.md+2
@@ -276,6 +276,8 @@ v1.1.20: /command returns errorCode command_not_found (HTTP 400) with did-you-me  v1.1.21: event-driven tab census + Claude session identity (John 2026-09-03: never poll the iframes again). GET /tabs and the SSE stream GET /tabs/events publish the extension host's own tabGroups census on every change; POST /claude/session-start ingests the Claude SessionStart hook (id + transcript path, before the jsonl is flushed) and GET /claude/sessions lists it; POST /claude/open-session opens an existing conversation by id and confirms the bind (retagging sdk-cli transcripts, which the Claude extension otherwise declines). Capability markers in /health: tabs.list, tabs.events, claude.open-session, claude.session-start, claude.sessions. +v1.1.79: no polling while a token is expired (the widget waits for the sign-in file to change, fifteen minutes as the safety net); a widget can tick on a time boundary instead of an interval.+ v1.1.78: an expired provider token is never "sign in again": the usage widgets say the token is waiting for Claude Code or Codex to refresh it, keep the last good numbers on screen dimmed, and retry every minute; the Claude widget skips the call when the file already says the token is expired.  v1.1.77: a new AI thread unlocks the active editor group first (VS Code keeps a group locked after its terminal tab closes and then refuses the new tab).
VERSION+1−1
@@ -1 +1 @@-1.1.78+1.1.79
bin/adom-vscode
⋯ 1 unchanged line ⋯
bin/hydrogen-widget-claude-usage

  
bin/hydrogen-widget-codex-usage

  
bin/hydrogen-widget-lib.py+16−5
@@ -61,7 +61,8 @@ function postWidth(){try{const w=document.querySelector('.w');if(!w)return;   document.body.appendChild(c);const total=Math.ceil(c.getBoundingClientRect().width)+2;c.remove();   if(total>0&&window.parent!==window)window.parent.postMessage({hdWidget:{width:total}},'*');}catch(e){}} async function tick(){try{const r=await fetch('/data.json',{cache:'no-store'});const d=await r.json();window.__last=d;render(d);}catch(e){document.getElementById('w').innerHTML='<span class="err">no data</span>';}}-let tm=null;function arm(){if(tm)return;tm=setInterval(tick,R);}function disarm(){if(tm){clearInterval(tm);tm=null;}}+/* 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();}}); tick();if(!document.hidden)arm(); /* The slot resizes when the Title Bar goes compact or tall; the iframe's own window gets the resize event and the@@ -69,16 +70,23 @@ 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 serve(title, data_fn, render_js, refresh_ms=60000, mark=None):+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}     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)))+        except Exception: return 0     def data():         with lock:-            if time.time() - cache['t'] >= cache['wait'] - 1:+            due = time.time() - cache['t'] >= cache['wait'] - 1+            if cache.get('expired_mtime') is not None and not due and _creds_mtime() != cache['expired_mtime']:+                due = True  # the sign-in file changed: try now+            if due:+                cache['expired_mtime'] = None                 try:                     cache['v'] = data_fn(); cache['wait'] = refresh_ms/1000                     if not cache['v'].get('error'): cache['good'] = cache['v']@@ -92,7 +100,10 @@ def serve(title, data_fn, render_js, refresh_ms=60000, mark=None):                     code = getattr(e, 'code', None)                     if code in (401, 403):                         msg = '%s token expired, waiting for %s to refresh it' % (title.replace(' usage', ''), 'Claude Code' if 'Claude' in title else 'Codex')-                        cache['wait'] = 60+                        # No polling while expired (John 2026-09-14: "stop that loop when it is not+                        # needed"): the next call happens when the sign-in file changes, or after+                        # 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)                     cache['v'] = {'error': msg, 'stale': cache['good']} if cache['good'] else {'error': msg}@@ -123,7 +134,7 @@ def serve(title, data_fn, render_js, refresh_ms=60000, mark=None):                         '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, '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['color'], mark['svg'])) if (mark and 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.py

  
install.sh

  
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-vscode",-  "version": "1.1.78",+  "version": "1.1.79",   "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.78",+  "version": "1.1.79",   "license": "PROPRIETARY",   "visibility": "public",   "hero": {@@ -141,4 +141,4 @@     "adom-vscode-extensions",     "adom-vscode-modes"   ]-}\ No newline at end of file+}