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.60
10 files changed
+147−4
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.60: ships two Hydrogen title-bar widgets as a hydrogen-widget.json facet (adom/hydrogen#17): claude-usage (5-hour, weekly and model-bucket percent used from the Claude Code sign-in) and codex-usage (the plan's rate-limit window from the Codex sign-in). Their scripts install to ~/.local/bin with the package; Hydrogen's Widgets pulldown opens them.+ v1.1.59: GET /stats also names the most recent out-of-memory victim (process, pid, rss) when the kernel log is readable, so Hydrogen Desktop can say what Linux killed. Read at most once per kill. v1.1.58: POST /fs/write (atomic file write as the workspace user) and GET /stats (memory, CPU ticks, OOM-kill count, load) so Hydrogen Desktop never spawns wsl.exe to land a screenshot or read its meter (adom/hydrogen#50, #76). Capability markers fs.write and stats in the /health roster; fs.write is hidden on cloud containers like exec.
VERSION+1−1@@ -1 +1 @@-1.1.59+1.1.60
bin/adom-vscode⋯ 1 unchanged line ⋯
bin/hydrogen-widget-claude-usageadded+23@@ -0,0 +1,23 @@+#!/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. Refreshes every minute."""+import json, os, sys+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))+from importlib.machinery import SourceFileLoader+lib = SourceFileLoader('hwlib', os.path.join(os.path.dirname(os.path.abspath(__file__)), 'hydrogen-widget-lib.py')).load_module()++def data():+ c = json.load(open(os.path.expanduser('~/.claude/.credentials.json')))+ oa = c.get('claudeAiOauth') or c+ tok = oa.get('accessToken')+ if not tok: return {'error': 'not signed in to Claude Code'}+ u = lib.http_json('https://api.anthropic.com/api/oauth/usage', {'Authorization': 'Bearer ' + tok, 'anthropic-beta': 'oauth-2025-04-20', 'User-Agent': 'hydrogen-widget-claude-usage'})+ out = {'five_hour': (u.get('five_hour') or {}).get('utilization'), 'seven_day': (u.get('seven_day') or {}).get('utilization'), 'models': []}+ for k, v in u.items():+ if k.startswith('seven_day_') and isinstance(v, dict) and v.get('utilization') is not None:+ out['models'].append({'name': k[len('seven_day_'):].replace('_', ' '), 'pct': v['utilization']})+ return out++RENDER = r"""(d.error?`<span class="err">${d.error}</span>`:bar('5h',d.five_hour??0)+bar('week',d.seven_day??0)+(d.models||[]).slice(0,1).map(m=>bar(m.name,m.pct)).join(''))"""+lib.serve('Claude usage', data, RENDER, 60000)
bin/hydrogen-widget-codex-usageadded+25@@ -0,0 +1,25 @@+#!/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. Refreshes every minute."""+import json, os, sys+from importlib.machinery import SourceFileLoader+lib = SourceFileLoader('hwlib', os.path.join(os.path.dirname(os.path.abspath(__file__)), 'hydrogen-widget-lib.py')).load_module()++def data():+ a = json.load(open(os.path.expanduser('~/.codex/auth.json')))+ t = a.get('tokens') or {}+ tok = t.get('access_token'); acct = t.get('account_id') or ''+ if not tok: return {'error': 'not signed in to Codex'}+ u = lib.http_json('https://chatgpt.com/backend-api/wham/usage', {'Authorization': 'Bearer ' + tok, 'ChatGPT-Account-Id': acct, 'User-Agent': 'hydrogen-widget-codex-usage', 'Accept': 'application/json'})+ rl = u.get('rate_limit') or {}+ p = rl.get('primary_window') or {}; s = rl.get('secondary_window') or {}+ 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++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):''))"""+lib.serve('Codex usage', data, RENDER, 60000)
bin/hydrogen-widget-lib.pyadded+65@@ -0,0 +1,65 @@+#!/usr/bin/env python3+"""Shared shape for the sample Hydrogen widgets: a tiny HTTP server that serves one page for+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)."""+import json, os, sys, time, threading, urllib.request+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer++PAGE = """<!doctype html><meta charset="utf-8"><title>%(title)s</title>+<style>+html,body{margin:0;height:100%%;background:transparent;color:#e6ebef;font:11px/1.2 "Familjen Grotesk","Satoshi",system-ui,sans-serif;overflow:hidden}+.w{display:flex;align-items:center;gap:6px;height:30px;padding:0 6px;white-space:nowrap}+.lbl{opacity:.75;font-size:9px;letter-spacing:.04em;text-transform:uppercase}+.bar{position:relative;width:38px;height:6px;border-radius:3px;background:rgba(255,255,255,.14);overflow:hidden}+.bar i{position:absolute;left:0;top:0;bottom:0;background:#19b3a6;border-radius:3px}+.bar.warn i{background:#f0a060}.bar.hot i{background:#e5484d}+.v{font-variant-numeric:tabular-nums;min-width:26px;text-align:right}+.big{font-size:14px;font-weight:600;font-variant-numeric:tabular-nums}+.err{color:#f0a060;font-size:10px}+</style><body><div class="w" id="w">%(title)s…</div>+<script>+const R=%(refresh_ms)d;+function bar(l,p){const c=p>=90?'hot':p>=70?'warn':'';return `<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>`;}+async function tick(){try{const r=await fetch('/data.json',{cache:'no-store'});const d=await r.json();document.getElementById('w').innerHTML=%(render_js)s;}catch(e){document.getElementById('w').innerHTML='<span class="err">no data</span>';}}+tick();setInterval(tick,R);+</script></body>"""++def serve(title, data_fn, render_js, refresh_ms=60000):+ port = int(os.environ.get('HYDROGEN_WIDGET_PORT') or (sys.argv[sys.argv.index('--port')+1] if '--port' in sys.argv else 8930))+ cache = {'t': 0, 'v': {'error': 'starting'}}+ lock = threading.Lock()+ def data():+ with lock:+ if time.time() - cache['t'] > refresh_ms/1000 - 1:+ try: cache['v'] = data_fn()+ except Exception as e: cache['v'] = {'error': str(e)[:120]}+ cache['t'] = time.time()+ return cache['v']+ 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_GET(self):+ if self.path.startswith('/data.json'): self._send(200, 'application/json', json.dumps(data()).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}).encode())+ srv = ThreadingHTTPServer(('127.0.0.1', port), H)+ # Tell Hydrogen we are up (the health poll would find us anyway; this is the explicit handshake).+ ctl = os.environ.get('ADOM_HD_CONTROL_URL'); wid = os.environ.get('HYDROGEN_WIDGET_ID')+ if ctl and wid:+ def announce():+ try:+ proxy = os.environ.get('HYDROGEN_PROXY_PORT', '')+ url = f"http://{port}.localhost:{proxy}/" if proxy else f"http://127.0.0.1:{port}/"+ req = urllib.request.Request(f"{ctl}/widgets/navigate", data=json.dumps({'id': wid, 'url': url}).encode(), headers={'content-type': 'application/json'})+ urllib.request.urlopen(req, timeout=5)+ except Exception: pass+ threading.Timer(0.5, announce).start()+ srv.serve_forever()++def http_json(url, headers):+ req = urllib.request.Request(url, headers=headers)+ with urllib.request.urlopen(req, timeout=15) as r: return json.loads(r.read().decode())
hydrogen-widget.jsonadded+25@@ -0,0 +1,25 @@+{+ "hydrogen_widget": "1.0.0",+ "widgets": [+ {+ "id": "claude-usage",+ "name": "Claude usage",+ "icon": "mdi:gauge",+ "brief": "Your Claude limits at a glance: 5-hour session, weekly, and the weekly model bucket that applies, as percent used. Reads the Claude Code sign-in in this workspace.",+ "width": 250,+ "install": "adom/adom-vscode",+ "launch": { "command": "hydrogen-widget-claude-usage", "port": 8931 },+ "health": { "url": "/", "timeout_seconds": 60 }+ },+ {+ "id": "codex-usage",+ "name": "Codex usage",+ "icon": "mdi:gauge-low",+ "brief": "Your Codex limit as percent used (and the secondary window when your plan has one). Reads the Codex sign-in in this workspace.",+ "width": 170,+ "install": "adom/adom-vscode",+ "launch": { "command": "hydrogen-widget-codex-usage", "port": 8932 },+ "health": { "url": "/", "timeout_seconds": 60 }+ }+ ]+}
install.sh+3@@ -64,3 +64,6 @@ echo "Installed the adom-vscode skill pack (parent + 6 sub-skills)." STAGED_VERSION="$(cat "$HERE/VERSION" 2>/dev/null || echo unknown)" "$BIN_DST" queue push system --type update-pending \ --payload "{\"component\":\"adom-vscode\",\"staged\":\"$STAGED_VERSION\"}" >/dev/null 2>&1 || true++# hydrogen-widget (adom/hydrogen#17): the Claude and Codex usage widgets for Hydrogen's title bar.+for w in "$HERE"/bin/hydrogen-widget-*; do [ -f "$w" ] && install -m 0755 "$w" "${HOME}/.local/bin/$(basename "$w")"; done
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-vscode",- "version": "1.1.59",+ "version": "1.1.60", "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.59",+ "version": "1.1.60", "license": "PROPRIETARY", "visibility": "public", "hero": {@@ -141,4 +141,4 @@ "adom-vscode-extensions", "adom-vscode-modes" ]-}+}