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.74
15 files changed
+33−24
README.md+2VERSION+1−1bin/adom-vscodebin/hydrogen-widget-claude-usagebin/hydrogen-widget-codex-usagebin/hydrogen-widget-lib.py+18−19bin/hydrogen-widget-marks.pyinstall.shpackage.json+1−1page.json+2−2widgets/README.md+9−1widgets/docs/claude-usage-compact.pngwidgets/docs/claude-usage.pngwidgets/docs/codex-usage-compact.pngwidgets/docs/codex-usage.pngREADME.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.74: the helper measures a widget on a hidden max-content clone (any markup, never fed by the slot width; the clock had asked for 361 px); the tall label clip applies to bar rows only; /preview?h= follows the width the widget asks for; crisp pictures of both layouts on the widgets page.+ v1.1.73: widget pages measure their intrinsic content, so a slot shrinks to what is shown (Codex in the compact bar) and grows to fit (Claude in the compact bar, three rows narrow when tall). v1.1.72: the widget helper serves /preview?w=&h= for clean docs screenshots of a widget at slot size.
VERSION+1−1@@ -1 +1 @@-1.1.73+1.1.74
bin/adom-vscode⋯ 1 unchanged line ⋯
bin/hydrogen-widget-claude-usagebin/hydrogen-widget-codex-usagebin/hydrogen-widget-lib.py+18−19@@ -14,7 +14,7 @@ html,body{margin:0;height:100%%;background:transparent;color:#e6ebef;font-family 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} .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}+.rows{display:flex;align-items:center;gap:8px;min-width:0;flex:1 1 auto} .row{display:flex;align-items:center;gap:4px} .lbl{opacity:.75;letter-spacing:.04em;text-transform:uppercase} .bar{position:relative;border-radius:3px;background:rgba(255,255,255,.14);overflow:hidden}@@ -36,8 +36,8 @@ html,body{margin:0;height:100%%;background:transparent;color:#e6ebef;font-family .w{font-size:9px;line-height:1;padding:2px 6px} .rows{flex-direction:column;align-items:stretch;justify-content:center;gap:2px;min-width:0} .row{display:grid;grid-template-columns:auto minmax(56px,1fr) auto;align-items:center;gap:4px;min-width:0}- .lbl{font-size:7.5px;max-width:5.5em;overflow:hidden;text-overflow:ellipsis}.bar{width:auto;height:5px}.v{font-size:9px;min-width:0}- .big{font-size:15px}.err{font-size:9px;white-space:normal;line-height:1.1}+ .lbl{font-size:7.5px}.row .lbl{max-width:5.5em;overflow:hidden;text-overflow:ellipsis}.bar{width:auto;height:5px}.v{font-size:9px;min-width:0}+ .big{font-size:15px}.err{font-size:9px;white-space:normal;line-height:1.1;max-width:130px} } @media (max-height:33px){ .err{overflow:hidden;text-overflow:ellipsis;max-width:100%%} } </style><body><div class="w"><span class="mark" style="color:%(mark_color)s">%(mark_svg)s</span><div class="rows" id="w">%(title)s…</div></div>@@ -45,18 +45,14 @@ html,body{margin:0;height:100%%;background:transparent;color:#e6ebef;font-family 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 render(d){document.getElementById('w').innerHTML=%(render_js)s;postWidth();}-/* THE WIDGET DECIDES ITS WIDTH: measure the content in the current layout (tall rows are narrow,- the compact row is wide) and ask the host for that width. Never fixed px; the host clamps. */-function postWidth(){try{const w=document.querySelector('.w');const rows=document.getElementById('w');if(!w||!rows)return;- /* Intrinsic width: rows stretch to the slot, so never measure a row's box. Sum each row's children- (a bar counts at its minimum in the tall layout, its fixed width in the short one) plus gaps. */- const tall=window.innerHeight>=34;let need=0;- for(const r of rows.children){const rs=getComputedStyle(r);const gap=parseFloat(rs.columnGap||rs.gap||'0')||0;let sum=0,n=0;- for(const c of r.children){n++;if(c.classList.contains('bar')){sum+=tall?56:c.getBoundingClientRect().width;}else{sum+=Math.ceil(c.scrollWidth);}}- if(n===0)sum=Math.ceil(r.scrollWidth);need=Math.max(need,sum+gap*Math.max(0,n-1));}- if(!tall){const rg=parseFloat(getComputedStyle(rows).gap||'0')||0;let total=0,n=0;for(const r of rows.children){n++;const rs=getComputedStyle(r);const gap=parseFloat(rs.columnGap||rs.gap||'0')||0;let sum=0,k=0;for(const c of r.children){k++;sum+=c.classList.contains('bar')?c.getBoundingClientRect().width:Math.ceil(c.scrollWidth);}if(k===0)sum=Math.ceil(r.scrollWidth);total+=sum+gap*Math.max(0,k-1);}need=total+rg*Math.max(0,n-1);}- const mark=w.querySelector('.mark');const cs=getComputedStyle(w);- const total=Math.ceil(need+(mark?mark.getBoundingClientRect().width+(parseFloat(cs.gap||'0')||0):0)+parseFloat(cs.paddingLeft)+parseFloat(cs.paddingRight)+3);+/* 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+ the slot, so measuring the live boxes would only ever grow). Works for any markup, not just rows. */+function postWidth(){try{const w=document.querySelector('.w');if(!w)return;+ const c=w.cloneNode(true);c.querySelectorAll('[id]').forEach(e=>e.removeAttribute('id'));+ c.style.cssText='position:absolute;left:-10000px;top:0;width:max-content;height:100vh;visibility:hidden;pointer-events:none';+ 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;}}@@ -91,18 +87,21 @@ def serve(title, data_fn, render_js, refresh_ms=60000, mark=None): def do_GET(self): if self.path.startswith('/data.json'): self._send(200, 'application/json', json.dumps(data()).encode()) elif self.path.startswith('/preview'):- # /preview?w=175&h=38 : this widget's own page in a frame of exactly that size, offset+ # /preview?h=38 (or ?w=175&h=38): this widget's own page in a frame of that height, offset # from the corner, so a browser screenshot of the frame (at any device scale) is a- # clean, crisp picture for docs. The Hydrogen slot never uses this; it is for humans.+ # clean, crisp picture for docs. Without w the frame follows the width the widget asks+ # for, exactly as the Title Bar slot does; with w it is pinned. The slot never uses this. from urllib.parse import urlparse, parse_qs q = parse_qs(urlparse(self.path).query)+ fixed = 'w' in q try: w = int(q.get('w', ['175'])[0]); h = int(q.get('h', ['38'])[0]) except ValueError: w, h = 175, 38 w = max(48, min(640, w)); h = max(16, min(120, h)) page = ('<!doctype html><meta charset="utf-8"><title>%s preview</title>' '<body style="margin:0;background:#0f1419"><div style="padding:150px 0 40px 40px">'- '<iframe id="w" src="/" style="width:%dpx;height:%dpx;border:1px solid #2c323a;border-radius:6px;background:#1c2128;display:block"></iframe>'- '</div></body>') % (title, w, h)+ '<iframe id="w" src="/" data-w="0" style="width:%dpx;height:%dpx;border:1px solid #2c323a;border-radius:6px;background:#1c2128;display:block"></iframe>'+ '</div><script>const fixed=%s;window.addEventListener("message",e=>{const m=e.data&&e.data.hdWidget;if(!m||!m.width)return;'+ '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_svg': ('<svg viewBox="0 0 16 16">' + mark['svg'] + '</svg>') if mark['svg'] else '', 'mark_color': mark['color']}).encode())
bin/hydrogen-widget-marks.pyinstall.shpackage.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-vscode",- "version": "1.1.73",+ "version": "1.1.74", "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.73",+ "version": "1.1.74", "license": "PROPRIETARY", "visibility": "public", "hero": {@@ -141,4 +141,4 @@ "adom-vscode-extensions", "adom-vscode-modes" ]-}+}
widgets/README.md+9−1@@ -6,6 +6,8 @@ Two small live panels for Hydrogen Desktop's Title Bar, showing how much of your  ++ Your session window (5h), your weekly window, and every weekly limit scoped to a model on your plan (Fable, Opus, ...), each as percent used, in the provider's own severity colour. In the full Title Bar the rows stack; in the compact Title Bar they sit in one row. Where the numbers come from: the Claude Code sign-in in your workspace (`~/.claude/.credentials.json`), asked at `api.anthropic.com/api/oauth/usage`, the same endpoint Claude Code's `/usage` panel reads. The answer's `limits` list is what is drawn. That call runs a model for nothing and costs no quota.@@ -14,6 +16,8 @@ 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. ## How often they ask@@ -39,11 +43,15 @@ widgets/bin/hydrogen-widget-marks.py the vendor marks, generated from the AI la widgets/bin/hydrogen-widget-claude-usage widgets/bin/hydrogen-widget-codex-usage widgets/README.md this page (linked from the pulldown row)-widgets/docs/*.png the pictures above+widgets/docs/*.png the pictures above, rendered from each widget's own /preview route ``` The package's `install.sh` symlinks `bin/hydrogen-widget-*` into `~/.local/bin`, which is where `launch.command` finds them. That is the whole integration; nothing in Hydrogen knows these two widgets by name. +## Pictures of a widget++Every widget built on the helper serves `/preview?h=38` (full Title Bar) and `/preview?h=20` (compact): its own page in a frame of that height, which follows the width the widget asks for, exactly as the slot does. A browser screenshot of the frame at a high device scale is a crisp picture for a page like this one; the pictures above were made that way with headless Chrome at three times scale. Pin the width with `?w=` when you want a fixed frame.+ ## Make your own The [hydrogen-widget skill](https://wiki.adom.inc/adom/hydrogen-bootstrap/files/skills/hydrogen-widget/SKILL.md) has the manifest fields, the launch contract, the tall and compact layouts, private repos, and a fifty-line clock to copy. The overview with pictures is the [Widgets page](https://wiki.adom.inc/adom/hydrogen/files/widgets/README.md) on the Hydrogen wiki.
widgets/docs/claude-usage-compact.pngadded⋯ 1 unchanged line ⋯
widgets/docs/claude-usage.png⋯ 1 unchanged line ⋯
widgets/docs/codex-usage-compact.pngadded⋯ 1 unchanged line ⋯
widgets/docs/codex-usage.png⋯ 1 unchanged line ⋯