← 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.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"   ]-}\ No newline at end of file+}