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.79
10 files changed
+22−9
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-usagebin/hydrogen-widget-codex-usagebin/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.pyinstall.shpackage.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" ]-}+}