app
adom-vscode-macos
Public Made by Adomby adom
macOS Hydrogen line of adom-vscode: the same CLI + VS Code extension (same extension id, same :8821 API) built natively for the arm64 Linux workspace Hydrogen runs on a Mac, minus the AI title bar (Hydrogen's agent bar and AI accounts popup own the title bar there). Installs over adom/adom-vscode; Hydrogen keeps this line registered. Upstream: adom-inc/adom-vscode.
← Commit history
Publish 1.1.112
17 files changed
+1179
LICENSE+31SKILL.md+122VERSION+1bin/adom-vscodebin/hydrogen-widget-claude-usage+48bin/hydrogen-widget-codex-usage+28bin/hydrogen-widget-lib.py+323bin/hydrogen-widget-marks.py+6install.sh+77package.json+77screenshots/hero.pngskills/adom-vscode-container/SKILL.md+88skills/adom-vscode-exec/SKILL.md+80skills/adom-vscode-extensions/SKILL.md+121skills/adom-vscode-queue/SKILL.md+57skills/adom-vscode-settings/SKILL.md+90uninstall.sh+30LICENSEadded+31@@ -0,0 +1,31 @@+Adom Proprietary License++Copyright (c) 2026 Adom Industries, Inc. All rights reserved.++This software (the "Software"), including the prebuilt binary distributed as+part of this package, is the proprietary property of Adom Industries, Inc.++Permitted use+- You may install and run the Software inside an Adom Docker container or+ any other environment provided by Adom Industries, Inc., for the purpose+ of using Adom products and services.++Restricted use+- You may not copy, modify, reverse-engineer, decompile, disassemble, or+ attempt to derive the source code of the Software.+- You may not redistribute, sublicense, sell, lease, or otherwise transfer+ the Software to any third party.+- The accompanying source code, where any is referenced, is not licensed+ for public use, modification, or redistribution and remains the+ confidential property of Adom Industries, Inc.++Disclaimer+The Software is provided "AS IS", without warranty of any kind, express or+implied, including but not limited to the warranties of merchantability,+fitness for a particular purpose, and noninfringement. In no event shall+Adom Industries, Inc. be liable for any claim, damages, or other liability,+whether in an action of contract, tort, or otherwise, arising from, out of,+or in connection with the Software or its use.++Contact+For licensing inquiries, contact [email protected].
SKILL.mdadded+122@@ -0,0 +1,122 @@+---+name: adom-vscode+description: "Control VS Code Server (code-server) from inside an Adom workspace (a cloud container, or the Hydrogen workspace machine on Windows/macOS). THIS is the wrapper for the VS Code FILE EXPLORER sidebar, editor tabs, markdown preview, etc., when the user says 'reveal in file explorer' or 'open in vscode' they mean this CLI, NOT adom-bridge (which controls Chrome browser windows via pup_*). START-HERE skill of the adom-vscode pack: file ops, workspace scoping, port discovery, and the map to the sub-skills covering live settings (adom-vscode-settings), container exec (adom-vscode-exec), the frontend event queue (adom-vscode-queue), container identity + api key (adom-vscode-container), extension management (adom-vscode-extensions). Trigger words: adom-vscode, open in vscode, reveal in explorer, file explorer, sidebar reveal, preview markdown, vscode command, vscode api, port 8821, port 8822, code-server control, drive vscode, vscode verbs."+---+> **macOS line.** This is `adom/adom-vscode-macos` (repo adom-inc/adom-vscode-macos), the build Hydrogen installs on a Mac: native arm64 for the Hydrogen workspace machine, same extension id, CLI and `:8821` API as `adom/adom-vscode`, **without the AI title bar** (Hydrogen's agent bar and AI accounts popup own the editor title bar there). Windows and cloud containers keep `adom/adom-vscode`. Upstream changes are merged in; the delta is `cli/src/main.rs` (no title-bar injection, strips upstream's, writes `~/.local/share/adom-vscode/flavor`) and `page-macos/`.++++# adom-vscode, VS Code Control (start here)++> **Disambiguation, read first.** When the user says "open in+> file explorer" / "reveal" / "show me where this file is" /+> "click to open in vscode" they almost always mean the+> **VS Code Server's sidebar Explorer** (the panel inside this+> workspace's running VS Code instance). That's THIS+> CLI: `adom-vscode reveal <path>`.+>+> **Do NOT** reach for `adom-bridge` for that, `adom-bridge`+> (formerly `adom-desktop`) controls puppeteer / Chrome browser+> windows via the `pup_*` verbs, not VS Code.+>+> **Do NOT** use the desktop `code` CLI either, it is not on+> PATH in code-server workspaces (`which code` returns nothing).+> The wrapper that talks to the running code-server's REST API+> (port 8821, or whatever `port.json` says) is this binary.+>+> If the user mentions "vscode", "explorer", or wants to see a+> file in the editor, default to `adom-vscode`. Save+> `adom-bridge` for cases where the trigger is clearly Chrome /+> browser / pup / shotlog tab.++## The mental model++A VS Code extension inside code-server runs an HTTP server on **127.0.0.1:8821**;+the `adom-vscode` CLI wraps those verbs with AI-oriented colored output. Everything+is plain HTTP too, so scripts and the HD/HW frontend hit the API directly.+`GET /health` returns the full verb roster (`verbs: [...]`) for feature detection.++If 8821 is taken the extension falls back to 8822..8831 (then an ephemeral port)+and writes the truth to `~/.local/share/adom-vscode/port.json`. The CLI resolves+the port automatically (`ADOM_VSCODE_PORT` env var, then port.json, then 8821);+any other caller should read that file when 8821 does not answer.++Binary: `~/.local/bin/adom-vscode` (also `/usr/local/bin/adom-vscode`).++## Skill map, the pack++| Skill | Read it for |+|---|---|+| **adom-vscode** (this file) | Mental model, file ops, workspace scoping, port discovery, management |+| **adom-vscode-settings** | Get/set ANY setting, theme, fonts, font sizes, all LIVE with no reload; resolved (truly rendered) font families |+| **adom-vscode-exec** | Run shell commands in the workspace, streamed (SSE) or buffered; why it beats an out-of-band host spawn |+| **adom-vscode-queue** | The container-to-frontend event queue: push/pull/status/clear, topics, TTL, persistence |+| **adom-vscode-container** | Workspace identity (hd = local Hydrogen workspace on Windows/macOS, hw = cloud), api-key status/inject, port discovery details, /health feature detection |+| **adom-vscode-extensions** | List/query/update extensions, the AI assistants (Claude/Codex/Kimi/Antigravity), the AI thread icons, `ai`/`tabs` verbs |++## CRITICAL: workspace scope is `/home/adom/project`++The Hydrogen panel boots code-server with `?folder=/home/adom/project`.+That is the workspace root, **VS Code can only see and operate on+paths inside that folder.** Anything outside it is invisible to the+Explorer sidebar and to most workspace-aware commands.++| Action | Outside `/home/adom/project` | Inside it |+|-------------------------------------|------------------------------|-----------|+| `adom-vscode open <file>` | Opens a loose tab (no Explorer link); the CLI returns `OK` because `vscode.open` accepts any path, but the user can't navigate to it | Works fully, tab opens AND the Explorer entry is highlightable |+| `adom-vscode reveal <path>` | Silently no-op; the extension's `revealInExplorer` accepts the call but the workspace doesn't contain the path so nothing visible happens. The CLI still returns `OK`. | Works, sidebar scrolls and selects the entry |+| `adom-vscode preview <file.md>` | Loose preview tab with no nav back to the source | Full preview + Explorer integration |++**Rule for AI-generated artifacts (thumbnails, dumps, smoke outputs,+intermediate files the user is going to look at):** put them inside+`/home/adom/project/...`, not in `/tmp/...`. Good homes:++- `/home/adom/project/<tool>/...`: colocated with the tool the artifact relates to.+- `/home/adom/project/.smoke/<task>/`: temporary smoke outputs; `.smoke/` is gitignored.+- `/home/adom/project/.scratch/`: quick throwaway exploration files.++**Symptom to watch for:** if you call `adom-vscode reveal /tmp/...` and the user+reports "I can't see it" or "your link doesn't do anything," the cause is almost+always workspace scoping. Move the artifact under `/home/adom/project/`, then+re-issue the reveal. Relative-path Markdown links in chat resolve against the+workspace root too; absolute `/home/adom/project/...` paths always click through.++## File operations+```bash+adom-vscode open /path/to/file.png # Open any file in VS Code tab+adom-vscode reveal /path/to/folder/ # Reveal in Explorer sidebar+adom-vscode preview /path/to/README.md # Markdown preview+```++## Generic command (escape hatch)+```bash+adom-vscode command workbench.action.reloadWindow+adom-vscode command workbench.action.toggleSidebarVisibility+```+For the full VS Code command reference, read `COMMANDS.md` in the adom-vscode repo.++## Notifications+```bash+adom-vscode notify "Build complete" --level info+adom-vscode notify "Check this" --level warning+adom-vscode notify "Failed" --level error+```++## Management+```bash+adom-vscode health # Version + actual port + full verb roster+adom-vscode install # Install everything: extension, skills, completions+adom-vscode reload # Reload VS Code window (warning: kills active Claude Code sessions)+```++## Output format+- Success: `OK: <what happened>` (green)+- Failure: `ERROR: <what went wrong>` (red) + `Hint: <next action>` (dim)+- Query verbs (`config get`, `theme get`, `font get`, `container`, `apikey status`,+ `queue pull/status`, `extensions list/status`) print pretty JSON.++## If extension not running+```bash+adom-vscode install+adom-vscode reload+```
VERSIONadded+1@@ -0,0 +1 @@+1.1.112
bin/adom-vscodeadded⋯ 1 unchanged line ⋯
bin/hydrogen-widget-claude-usageadded+48@@ -0,0 +1,48 @@+#!/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. Asks the provider at most once every three minutes, pauses while the window is hidden, retries at 5, 15, 45 s on errors. Click the slot to cycle the views (rows, one number, a ring, a pair, all numbers, all rings)."""+import json, os, sys, time+sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))+from importlib.machinery import SourceFileLoader+marks = SourceFileLoader('hwmarks', os.path.join(os.path.dirname(os.path.realpath(__file__)), 'hydrogen-widget-marks.py')).load_module()+lib = SourceFileLoader('hwlib', os.path.join(os.path.dirname(os.path.realpath(__file__)), 'hydrogen-widget-lib.py')).load_module()++def data():+ if not os.path.exists(os.path.expanduser('~/.claude/.credentials.json')):+ return {'error': 'sign in to Claude Code in this workspace'}+ c = lib.read_json_if_changed('~/.claude/.credentials.json')+ oa = c.get('claudeAiOauth') or c+ tok = oa.get('accessToken')+ if not tok: return {'error': 'not signed in to Claude Code'}+ exp = oa.get('expiresAt') or 0+ if exp and exp < time.time() * 1000:+ # Do not spend a call on a token the file already says is expired; Claude Code refreshes+ # it on its next run and read_json_if_changed picks the new file up.+ e = Exception('token expired'); e.code = 401; raise e+ 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'})+ bars = []+ # The structured list is what Claude Code's own Usage panel draws: the session window, the+ # weekly window, and every weekly limit scoped to a model (Fable, Opus, ...), each with the+ # provider's severity. Fall back to the flat fields on an older response.+ for lim in u.get('limits') or []:+ kind = lim.get('kind'); pct = lim.get('percent')+ if pct is None: continue+ if kind == 'session': label = '5h'+ elif kind == 'weekly_all': label = 'week'+ elif kind == 'weekly_scoped':+ scope = lim.get('scope') or {}; model = scope.get('model') or {}+ label = model.get('display_name') or 'model'+ else: label = kind or 'limit'+ bars.append({'label': label, 'pct': pct, 'severity': lim.get('severity') or 'normal'})+ if not bars:+ for label, key in (('5h', 'five_hour'), ('week', 'seven_day')):+ v = (u.get(key) or {}).get('utilization')+ if v is not None: bars.append({'label': label, 'pct': v, 'severity': 'normal'})+ for k, v in u.items():+ if k.startswith('seven_day_') and isinstance(v, dict) and v.get('utilization') is not None:+ bars.append({'label': k[len('seven_day_'):].replace('_', ' '), 'pct': v['utilization'], 'severity': 'normal'})+ return {'items': bars}++RENDER = r"""(d.error?`<span class="err">${d.error}</span>`:(d.items||[]).map(b=>bar(b.label,b.pct,b.severity)).join(''))"""+lib.serve('Claude usage', data, RENDER, 180000, mark=marks.MARKS['claude'], default_view='nums')
bin/hydrogen-widget-codex-usageadded+28@@ -0,0 +1,28 @@+#!/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. Asks the provider at most once every three minutes, pauses while the window is hidden, retries at 5, 15, 45 s on errors. Click the slot to cycle the views (rows, one number, a ring, the pair, both as numbers or rings)."""+import json, os, sys+from importlib.machinery import SourceFileLoader+marks = SourceFileLoader('hwmarks', os.path.join(os.path.dirname(os.path.realpath(__file__)), 'hydrogen-widget-marks.py')).load_module()+lib = SourceFileLoader('hwlib', os.path.join(os.path.dirname(os.path.realpath(__file__)), 'hydrogen-widget-lib.py')).load_module()++def data():+ if not os.path.exists(os.path.expanduser('~/.codex/auth.json')):+ return {'error': 'sign in to Codex in this workspace'}+ a = lib.read_json_if_changed('~/.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")+ items = [{'label': label(p), 'pct': p.get('used_percent', 0) or 0, 'severity': 'normal'}]+ if s: items.append({'label': label(s), 'pct': s.get('used_percent', 0) or 0, 'severity': 'normal'})+ return {'plan': u.get('plan_type'), 'items': items}++RENDER = r"""(d.error?`<span class="err">${d.error}</span>`:(d.items||[]).map(b=>bar(b.label,b.pct,b.severity)).join(''))"""+lib.serve('Codex usage', data, RENDER, 180000, mark=marks.MARKS['codex'], default_view='nums')
bin/hydrogen-widget-lib.pyadded+323@@ -0,0 +1,323 @@+#!/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).++Views (John 2026-09-15, "the font size is too small"): a widget that returns `items`+(label, pct, severity) gets a set of layouts for free and the user clicks the slot to cycle+them: every meter as rows (the original), one item alone as a big number, one item as a ring+gauge, any pair side by side, all items as numbers, all items as rings. Each view asks the+Title Bar for exactly the width it needs, so a single number takes a third of the space the+rows did and its digits fill the slot's height. The chosen view is remembered on disk.++Robustness (John 2026-09-15, "HTTP Error 404" after a reboot): the last good numbers are+saved to disk and shown dimmed from the first second after a restart; a provider error is said+in plain words ("no answer from OpenAI (HTTP 404), retrying") and retried at 5, 15, 45 s+before backing off; the page polls again at the server's own retry time, never three minutes+later."""+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-family:"Familjen Grotesk","Satoshi",system-ui,sans-serif;overflow:hidden}+/* Two layouts, chosen by the slot's height (the Title Bar, full or compact), like the meter:+ 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;cursor:pointer;user-select:none}+.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 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}+.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;text-align:right}+.big{font-weight:600;font-variant-numeric:tabular-nums}+.err{color:#f0a060}+/* An error is a button: a click checks again now (John 2026-09-17: "make this clickable so i can+ manually refresh it after i login", and "i can't read this text its so small"). */+#wrap.act{cursor:pointer}#wrap.act:hover .err{text-decoration:underline;text-underline-offset:2px}+.checking{opacity:.7}+/* Stale (last numbers while the provider is between answers): a light fade only, so the colours+ read the same as a fresh neighbour (John 2026-09-15: "the claude one has washed out colors"). */+#w.stale{opacity:.78}+/* Views: a big number per item (cell), pairs with a hairline between (sep), ring gauges. */+.cell{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:1px;min-width:0}+.pct{font-weight:700;font-variant-numeric:tabular-nums;line-height:1;color:#19b3a6;letter-spacing:-.01em}+.pct.warn{color:#f0a060}.pct.hot{color:#e5484d}+.sep{flex:none;width:1px;align-self:stretch;background:rgba(255,255,255,.18);margin:0 2px}+.rw{display:flex;align-items:center;gap:4px}+.ring{position:relative;flex:none}.ring svg{width:100%%;height:100%%;transform:rotate(-90deg);display:block}+.ring .in{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-weight:700;font-variant-numeric:tabular-nums;line-height:1}+@media (max-height:33px){+ .w{font-size:clamp(7px,37vh,12px);line-height:1.2}+ .lbl{font-size:clamp(6px,30vh,9px)}.bar{width:clamp(24px,130vh,40px);height:clamp(4px,20vh,6px)}.v{min-width:2.4em}+ .big{font-size:clamp(9px,47vh,14px)}.err{font-size:clamp(9px,52vh,12px)}+ .mark{width:clamp(10px,55vh,17px);height:clamp(10px,55vh,17px)}+ .cell{flex-direction:row;align-items:baseline;gap:3px}+ .pct{font-size:clamp(9px,66vh,14px)}.cell .lbl{font-size:clamp(6px,32vh,8px)}+ .ring{width:clamp(10px,80vh,17px);height:clamp(10px,80vh,17px)}.ring .in{display:none}+}+@media (min-height:34px){+ /* Tall: rows stacked. Columns take their natural width (label, value) and the bar takes the+ rest, so nothing is ever clipped whatever the slot width; a long label is trimmed with an+ ellipsis instead of pushing the value out of view. */+ /* Narrow on purpose (John 2026-09-14: "make this 1/3rd the width"): the bar is a short gauge,+ not a ruler; the label and value carry the number. About 90 px for three rows with a mark. */+ .w{font-size:8px;line-height:1;padding:1px 4px;gap:4px}+ .mark{width:13px;height:13px}+ .rows{flex-direction:column;align-items:stretch;justify-content:center;gap:2px;min-width:0}+ .rows.hz{flex-direction:row;align-items:center;gap:6px}+ .row{display:grid;grid-template-columns:auto minmax(18px,1fr) auto;align-items:center;gap:3px;min-width:0}+ .lbl{font-size:6.5px}.row .lbl{max-width:4.2em;overflow:hidden;text-overflow:ellipsis}.bar{width:auto;height:4px}.v{font-size:8px;min-width:0}+ .big{font-size:14px}.err{font-size:11.5px;white-space:normal;line-height:1.15;max-width:150px}+ /* Big numbers fill the 36 px the slot gives: 22 px digits over a 7 px label. */+ .cell .lbl{font-size:7px}.pct{font-size:22px}+ /* One percentage size in every view (John 2026-09-15: Claude's all-numbers view sat at 11.6 px+ beside Codex's single number at 13.2 px, and the two widgets are always side by side). */+ .nums .cell{gap:2px}+ .ring{width:30px;height:30px}.ring .in{font-size:9.5px}+ .rw .cell .lbl{font-size:7px}+}+@media (max-height:33px){ .err{overflow:hidden;text-overflow:ellipsis;max-width:100%%} }+</style><body><div class="w" id="wrap">%(mark_html)s<div class="rows" id="w">%(title)s…</div></div>+<script>+const R=%(refresh_ms)d;+function sev(p,s){return (s==='critical'||p>=90)?'hot':(s==='warning'||p>=70)?'warn':'';}+function bar(l,p,s){const c=sev(p,s);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 cell(it){return `<span class="cell"><span class="lbl">${it.label}</span><span class="pct ${sev(it.pct,it.severity)}">${Math.round(it.pct)}%%</span></span>`;}+function ring(it,withLabel){const p=Math.max(0,Math.min(100,it.pct));const r=15.5,c=2*Math.PI*r;const col={hot:'#e5484d',warn:'#f0a060'}[sev(it.pct,it.severity)]||'#19b3a6';+ const g=`<span class="ring"><svg viewBox="0 0 36 36"><circle cx="18" cy="18" r="${r}" fill="none" stroke="rgba(255,255,255,.14)" stroke-width="4.5"/><circle cx="18" cy="18" r="${r}" fill="none" stroke="${col}" stroke-width="4.5" stroke-dasharray="${(p/100*c).toFixed(1)} ${c.toFixed(1)}" stroke-linecap="round"/></svg><span class="in">${Math.round(p)}</span></span>`;+ const compact=window.innerHeight<=33;+ const lab=compact?`<span class="cell"><span class="lbl">${it.label}</span><span class="pct ${sev(it.pct,it.severity)}">${Math.round(p)}%%</span></span>`:`<span class="lbl">${it.label}</span>`;+ return `<span class="rw">${g}${withLabel?lab:''}</span>`;}+/* The views a set of items offers, in click order. */+function viewsFor(items){const n=items.length;const v=[{id:'rows',name:'all meters'}];+ items.forEach((it,i)=>v.push({id:'big:'+i,name:it.label+' only'}));+ items.forEach((it,i)=>v.push({id:'ring:'+i,name:it.label+' as a ring'}));+ for(let i=0;i<n;i++)for(let j=i+1;j<n;j++)v.push({id:'duo:'+i+','+j,name:items[i].label+' + '+items[j].label});+ if(n>1){v.push({id:'nums',name:'all as numbers'});v.push({id:'rings',name:'all as rings'});}+ return v;}+/* A widget whose default is 'nums' (all as numbers) and that has a single limit gets that limit as+ one big number, the same look: 'nums' needs two items, and falling to the first view (rows) showed+ Codex's lone week as a thin meter (John 2026-10-01). */+const DEF_VIEW='%(default_view)s';+let curView=(function(){try{return localStorage.getItem('hw.view')||DEF_VIEW;}catch(e){return DEF_VIEW;}})();+function renderItems(items,id){const [kind,arg]=String(id).split(':');const at=(k)=>items[Math.max(0,Math.min(items.length-1,+k||0))];+ switch(kind){+ case 'big':return cell(at(arg));+ case 'ring':return ring(at(arg),true);+ case 'duo':{const [a,b]=String(arg||'0,1').split(',');return cell(at(a))+'<span class="sep"></span>'+cell(at(b));}+ case 'nums':return '<span class="nums rw">'+items.map(cell).join('<span class="sep"></span>')+'</span>';+ case 'rings':return '<span class="nums rw">'+items.map(it=>ring(it,true)).join('')+'</span>';+ default:return items.map(b=>bar(b.label,b.pct,b.severity)).join('');}}+/* d.stale = the last good data while the provider is unreachable or the token is between refreshes:+ drawn dimmed, the reason in the tooltip, never an error where numbers were. */+function render(d0){const d=d0&&d0.stale?d0.stale:d0;const el=document.getElementById('w');const wrap=document.getElementById('wrap');+ el.classList.toggle('stale',!!(d0&&d0.stale));let tip=(d0&&d0.stale)?String(d0.error||''):'';+ if(d0&&d0.view&&d0.view!==curView){curView=d0.view;try{localStorage.setItem('hw.view',curView);}catch(e){}}+ if(d&&Array.isArray(d.items)&&d.items.length){const vs=viewsFor(d.items);let k=vs.findIndex(v=>v.id===curView);if(k<0){k=vs.findIndex(v=>v.id===DEF_VIEW);if(k<0&&DEF_VIEW==='nums')k=vs.findIndex(v=>v.id==='big:0');if(k<0)k=0;curView=vs[k].id;}+ el.classList.toggle('hz',curView!=='rows');el.innerHTML=renderItems(d.items,curView);+ tip=(tip?tip+'. ':'')+'%(title)s: '+vs[k].name+'. Click for the next view ('+(k+1)+' of '+vs.length+').';}+ else{el.classList.remove('hz');el.innerHTML=%(render_js)s;tip=(d0&&d0.error?String(d0.error)+'. ':'')+'Click to check again now.';}+ wrap.classList.toggle('act',!!(d0&&d0.error));wrap.title=tip;postWidth();}+/* The click's own re-render must carry the NEW view: window.__last is the last poll's copy, whose+ view field still names the old one, and render() adopts the server's view over the local one.+ Without this line a click changed the server and nothing on screen until the next poll, three+ minutes later (Ray, adom/hydrogen#81, 2026-09-15). */+function setView(id){curView=id;try{localStorage.setItem('hw.view',id);}catch(e){}+ if(window.__last)window.__last.view=id;+ fetch('/view',{method:'POST',body:id}).then(r=>{if(!r.ok)throw new Error('http '+r.status);}).catch(()=>{setTimeout(()=>fetch('/view',{method:'POST',body:id}).catch(()=>{}),3000);});+ if(window.__last)render(window.__last);}+/* CHECK NOW: POST /refresh makes the server ask the provider (or re-read the sign-in file) right away+ instead of at its next scheduled check, and answers with the fresh data. One call per click. */+let refreshing=false;+async function refreshNow(showChecking){if(refreshing)return;refreshing=true;+ if(showChecking){const el=document.getElementById('w');el.classList.remove('hz');el.innerHTML='<span class="err checking">checking…</span>';postWidth();}+ try{const r=await fetch('/refresh',{method:'POST',cache:'no-store'});if(!r.ok)throw new Error('http '+r.status);const d=await r.json();window.__last=d;render(d);}+ catch(e){tick();}finally{refreshing=false;}}+document.getElementById('wrap').addEventListener('click',()=>{const d0=window.__last;const d=d0&&d0.stale?d0.stale:d0;+ if(d0&&d0.error){if(!d||!Array.isArray(d.items)||!d.items.length){refreshNow(true);return;}refreshNow(false);}+ if(!d||!Array.isArray(d.items)||!d.items.length)return;+ const vs=viewsFor(d.items);const k=vs.findIndex(v=>v.id===curView);setView(vs[(k+1)%%vs.length].id);});+/* 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){}}+/* A failed poll (the widget process restarting, the proxy between the page and it) shows the last+ data dimmed and tries again in 5 s, then 15, then 45, then the normal interval: never "no data"+ for three minutes over a blip (AdomLapper 2026-09-14). When the server itself is waiting on the+ provider it says when it will try again (retry_in) and the page asks again right after. */+let fails=0,qt=null;function quick(ms){clearTimeout(qt);qt=setTimeout(tick,ms);}+async function tick(){try{const r=await fetch('/data.json',{cache:'no-store'});if(!r.ok)throw new Error('http '+r.status);const d=await r.json();fails=0;window.__last=d;render(d);+ if(d.error&&typeof d.retry_in==='number'&&d.retry_in*1000<R)quick(Math.max(2000,d.retry_in*1000+500));}+ catch(e){fails++;if(window.__last){render({error:'no answer from the widget, retrying',stale:window.__last.stale||window.__last});}else{render({error:'no answer from the widget, retrying'});}+ quick([5000,15000,45000][Math.min(fails-1,2)]);}}+/* 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+ height media queries above re-evaluate. Re-render too, for widgets whose render depends on the size. */+let rz=null;window.addEventListener('resize',()=>{clearTimeout(rz);rz=setTimeout(()=>{if(window.__last)render(window.__last);},80);});+</script></body>"""++def _state_path(title):+ d = os.path.expanduser('~/.adom/widgets'); os.makedirs(d, exist_ok=True)+ return os.path.join(d, ''.join(ch if ch.isalnum() else '-' for ch in title.lower()) + '.state.json')++def _load_state(title):+ try: return json.load(open(_state_path(title)))+ except Exception: return {}++def _save_state(title, st):+ try:+ p = _state_path(title); tmp = p + '.tmp'+ json.dump(st, open(tmp, 'w')); os.replace(tmp, p)+ except Exception: pass++def serve(title, data_fn, render_js, refresh_ms=60000, mark=None, align_ms=0, default_view='rows'):+ 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))+ provider = 'Anthropic' if 'Claude' in title else ('OpenAI' if 'Codex' in title else 'the provider')+ # One upstream call per interval at most, however often the page asks. On an upstream error+ # the next tries come at 5, 15, 45, 90 s, then minutes, capped at 15; back to normal on the+ # next success. The last good numbers live on disk so a restart shows them at once, dimmed.+ st = _load_state(title)+ # The view a fresh install opens on: the widget's choice (Claude and Codex: every limit as a+ # big number in a row, John 2026-10-01; Codex was "the week only" from 2026-09-19), until the+ # user clicks to another one, which is remembered on disk and wins from then on.+ cache = {'t': 0, 'v': {'error': 'starting'}, 'wait': refresh_ms/1000, 'good': None, 'fails': 0, 'view': str(st.get('view') or default_view)}+ if isinstance(st.get('good'), dict) and st['good'].get('items') is not None:+ cache['good'] = st['good']+ when = time.strftime('%H:%M', time.localtime(st.get('at') or 0)) if st.get('at') else 'earlier'+ cache['v'] = {'error': 'waiting for %s, last numbers from %s' % (provider, when), 'stale': cache['good']}+ 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:+ 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']; cache['fails'] = 0+ _save_state(title, {'good': cache['good'], 'at': time.time(), 'view': cache['view']})+ except Exception as e:+ # A 401/403 is almost always an EXPIRED access token, not a missing sign-in:+ # Claude Code and Codex refresh their own tokens the next time they run, and+ # the widget re-reads the file. Never tell a signed-in user to sign in again+ # (AdomLapper 2026-09-14: "sign in to Claude again" while John was chatting).+ # Say what is true, keep the last good numbers on screen (dimmed), and retry+ # every minute instead of backing off; other errors still back off.+ 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')+ # 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:+ # Plain words, never a raw exception (AdomLapper 2026-09-15 after a reboot:+ # "HTTP Error 404: Not Found" sat in the Title Bar for fifteen minutes while+ # the endpoint answered 200 to a manual call a minute later).+ reason = ('HTTP %s' % code) if code else ('network' if 'URLError' in type(e).__name__ or 'timed out' in str(e) else type(e).__name__)+ msg = 'no answer from %s (%s), retrying' % (provider, reason)+ cache['fails'] += 1+ cache['wait'] = [5, 15, 45, 90, 180, 300, 600, 900][min(cache['fails'] - 1, 7)]+ cache['v'] = {'error': msg, 'stale': cache['good']} if cache['good'] else {'error': msg}+ cache['t'] = time.time()+ out = dict(cache['v']); out['view'] = cache['view']+ if out.get('error'): out['retry_in'] = max(0, int(cache['wait'] - (time.time() - cache['t'])))+ return out+ def refresh_now():+ # A click on the widget: make the next data() call due now, whatever the schedule says.+ with lock:+ cache['t'] = 0; cache['expired_mtime'] = None+ return data()+ def set_view(v):+ with lock:+ cache['view'] = v+ s = _load_state(title); s['view'] = v; _save_state(title, s)+ 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_POST(self):+ if self.path.startswith('/view'):+ n = int(self.headers.get('content-length') or 0)+ v = self.rfile.read(n).decode(errors='replace').strip()[:40] if n else ''+ if v: set_view(v)+ self._send(200, 'application/json', json.dumps({'ok': True, 'view': cache['view']}).encode())+ elif self.path.startswith('/refresh'):+ self._send(200, 'application/json', json.dumps(refresh_now()).encode())+ else: self._send(404, 'text/plain', b'not found')+ 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?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. 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="/" 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, 'default_view': default_view, '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.get('color', 'currentColor'), mark.get('svg', ''))) if 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+ srv = None+ for attempt in range(10):+ try:+ srv = ThreadingHTTPServer(('127.0.0.1', port), H); break+ except OSError as e:+ if attempt == 9: raise+ time.sleep(0.5)+ # 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()++_creds = {}+def read_json_if_changed(path):+ """Parse a JSON file only when its mtime moved; the parsed value is cached otherwise."""+ path = os.path.expanduser(path)+ st = os.stat(path)+ ent = _creds.get(path)+ if ent and ent[0] == st.st_mtime_ns: return ent[1]+ v = json.load(open(path)); _creds[path] = (st.st_mtime_ns, v); return v++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())
bin/hydrogen-widget-marks.pyadded+6@@ -0,0 +1,6 @@+"""Vendor marks, generated from browser/ai-titlebar.js (the AI launch bar): the same artwork+and colours users already know from the editor. Do not edit; regenerate."""+MARKS = {+ "claude": {"svg": '<g transform="scale(0.0645)"><path fill="currentColor" d="M52.4285 162.873L98.7844 136.879L99.5485 134.602L98.7844 133.334H96.4921L88.7237 132.862L62.2346 132.153L39.3113 131.207L17.0249 130.026L11.4214 128.844L6.2 121.873L6.7094 118.447L11.4214 115.257L18.171 115.847L33.0711 116.911L55.485 118.447L71.6586 119.392L95.728 121.873H99.5485L100.058 120.337L98.7844 119.392L97.7656 118.447L74.5877 102.732L49.4995 86.1905L36.3823 76.62L29.3779 71.7757L25.7854 67.1678L24.2572 57.2427L30.6511 50.1533L39.3113 50.7441L41.5205 51.3349L50.2643 58.0698L68.9853 72.4849L93.4359 90.4432L96.9848 93.3972L98.4111 92.3901L98.5808 91.6812L96.9848 88.9635L83.7836 65.0987L69.6647 40.8793L63.4406 30.8361L61.7427 24.8099C61.2334 22.3284 60.7241 20.2018 60.7241 17.6021L68.0084 7.71947L72.0871 6.41968L81.7658 7.71947L85.8446 11.2643L91.9174 25.1645L101.766 46.9043L117.001 76.6199L121.42 85.4033L123.799 93.5154L124.648 95.9969H126.176V94.5787L127.365 78.6265L129.574 59.1298L131.782 34.0801L132.461 26.9906L135.859 18.6603L142.763 13.0964L148.157 15.6961L152.575 21.9587L151.896 25.9761L149.176 43.4573L143.951 70.9977L140.554 89.4287H142.593L144.971 87.0654L154.31 74.7811L170.054 55.2844L177.128 47.4498L185.115 38.9012L190.34 34.7656H200.019L207.093 45.2049L203.865 56.0578L193.917 68.7554L185.624 79.4128L173.792 95.2905L166.378 108.089L167.057 109.096L168.755 108.919L195.445 103.207L209.812 100.607L227.07 97.6528L234.823 101.316L235.672 105.017L232.614 112.579L214.187 117.128L192.559 121.44L160.392 129.079L159.882 129.374L160.392 129.965L174.929 131.325L181.153 131.68H196.388L224.69 133.807L232.104 138.71L236.522 144.677L235.842 149.226L224.52 154.965L209.133 151.353L173.282 142.786L160.902 139.717H159.203V140.782L169.492 150.821L188.258 167.777L211.851 189.635L213.04 195.011L210.002 199.324L206.773 198.851L185.965 183.15L177.887 176.061L159.882 160.892H158.693V162.489L162.77 168.574L184.767 201.687L185.965 211.845L184.427 215.153L178.716 217.162L172.432 216.03L159.543 197.848L146.315 177.541L135.518 159.237L134.159 159.946L127.875 228.319L124.987 231.746L118.193 234.346L112.481 230.028L109.593 222.902L112.481 208.996L116.049 190.874L118.872 176.406L121.42 158.469L122.949 152.502L122.779 152.089L121.59 152.325L108.913 169.759L89.5688 195.898L74.3336 212.201L70.7659 213.68L64.4807 210.371L65.0692 204.581L68.6369 199.442L89.5688 172.831L102.077 156.409L110.28 146.883L110.11 145.522H109.593L54.1264 181.553L44.1084 182.913L39.8228 178.831L40.3322 172.24L42.3716 170.113L59.2917 158.469L52.4285 162.873Z"/></g>', "color": "#d97757"},+ "codex": {"svg": '<g transform="scale(0.6667)"><path fill="currentColor" d="M22.28 9.82a5.98 5.98 0 0 0-.52-4.91 6.05 6.05 0 0 0-6.51-2.9A6.07 6.07 0 0 0 4.98 4.18a5.98 5.98 0 0 0-4 2.9 6.05 6.05 0 0 0 .74 7.1 5.98 5.98 0 0 0 .51 4.91 6.05 6.05 0 0 0 6.51 2.9A5.98 5.98 0 0 0 13.26 24a6.06 6.06 0 0 0 5.77-4.21 5.99 5.99 0 0 0 4-2.9 6.06 6.06 0 0 0-.75-7.07zm-9.02 12.61a4.48 4.48 0 0 1-2.88-1.04l.14-.08 4.78-2.76a.79.79 0 0 0 .39-.68v-6.74l2.02 1.17a.07.07 0 0 1 .04.05v5.58a4.5 4.5 0 0 1-4.49 4.5zm-9.66-4.13a4.47 4.47 0 0 1-.54-3.01l.14.09 4.78 2.76a.77.77 0 0 0 .78 0l5.84-3.37v2.33a.08.08 0 0 1-.03.06l-4.83 2.79a4.5 4.5 0 0 1-6.14-1.65zM2.34 7.9a4.48 4.48 0 0 1 2.37-1.97v5.68a.77.77 0 0 0 .38.67l5.81 3.36-2.02 1.17a.08.08 0 0 1-.07 0l-4.83-2.79A4.5 4.5 0 0 1 2.34 7.9zm16.6 3.86l-5.84-3.37 2.02-1.17a.08.08 0 0 1 .07 0l4.83 2.79a4.49 4.49 0 0 1-.69 8.1v-5.68a.79.79 0 0 0-.39-.67zm2.01-3.02l-.14-.09-4.78-2.77a.78.78 0 0 0-.78 0L9.42 9.24V6.91a.07.07 0 0 1 .03-.06l4.83-2.79a4.5 4.5 0 0 1 6.68 4.66zM8.32 12.86l-2.02-1.16a.08.08 0 0 1-.04-.06V6.06a4.5 4.5 0 0 1 7.38-3.45l-.14.08-4.78 2.76a.79.79 0 0 0-.39.68zm1.1-2.37l2.6-1.5 2.6 1.5v3l-2.6 1.5-2.6-1.5z"/></g>', "color": "#e6ebef"},+}
install.shadded+77@@ -0,0 +1,77 @@+#!/usr/bin/env bash+# adom-wiki pkg install script for adom-vscode-macos (the macOS Hydrogen line of adom-vscode).+# Same layout as adom/adom-vscode's; the CLI's `install` strips the upstream AI title-bar+# script and writes ~/.local/share/adom-vscode/flavor so Hydrogen can tell this line apart.+#+# This script runs as the regular user — `needs_sudo: false` in package.json,+# matching every other adom-wiki CLI (adom-google, adom-mouser, adom-tts, the+# parts CLIs, etc.). The binary goes into ~/.local/bin so daemon-managed+# updates never need a sudo gate.+#+# The CLI itself (`adom-vscode install`, invoked at the bottom of this script)+# will SELF-ELEVATE to sudo for the one step that truly needs root — patching+# /usr/lib/code-server/lib/vscode/.../workbench.html to install the+# paste-screenshot interceptor that lets users Ctrl+V images into a Claude CLI+# terminal. That patch is idempotent (re-runs return "Browser scripts already+# up to date") so most installs never invoke sudo at all; only the first patch+# (or one that follows a code-server upgrade that overwrote workbench.html, or+# one that follows a PASTE_SCREENSHOT_JS change) actually shells `sudo cp`.+#+# Self-elevation is safe here because adom-vscode is an Adom-container-only+# tool, and Adom containers grant the user passwordless sudo by design.+# Hooking into workbench.html is load-bearing for the Claude CLI image-paste+# UX — and any future browser-side instrumentation we want to add into+# code-server's main HTML load — so we keep it explicit rather than degrade.++set -euo pipefail++HERE="$(cd "$(dirname "$0")" && pwd)"+BIN_SRC="$HERE/bin/adom-vscode"+BIN_DST="${HOME}/.local/bin/adom-vscode"++if [ ! -f "$BIN_SRC" ]; then+ echo "ERROR: $BIN_SRC missing from package payload" >&2+ exit 1+fi++mkdir -p "$(dirname "$BIN_DST")"+install -m 0755 "$BIN_SRC" "$BIN_DST"+echo "Installed adom-vscode -> $BIN_DST"++case ":${PATH}:" in+ *":${HOME}/.local/bin:"*) ;;+ *) echo "Note: $HOME/.local/bin is not on PATH. Add: export PATH=\"\$HOME/.local/bin:\$PATH\"" ;;+esac++# Install the skill pack from the tarball (offline-safe; the CLI install+# below refreshes each skill from the wiki when reachable).+SKILLS_DEST="$HOME/.claude/skills"+install_skill() { mkdir -p "$SKILLS_DEST/$1"; cp "$2" "$SKILLS_DEST/$1/SKILL.md"; }+install_skill adom-vscode "$HERE/SKILL.md"+# Every sub-skill the package ships, discovered, never listed by hand: a hand-kept list broke+# 1.1.97 (the retired modes skill was removed from the package, this script still copied it,+# and set -e stopped the install before the VSIX and the CLI's install step ran).+N=0+for d in "$HERE"/skills/*/; do+ [ -f "$d/SKILL.md" ] || continue+ install_skill "$(basename "$d")" "$d/SKILL.md"; N=$((N+1))+done+rm -rf "$SKILLS_DEST/adom-vscode-modes" # retired in 1.1.97+echo "Installed the adom-vscode skill pack (parent + $N sub-skills)."++# The CLI handles the rest, including self-elevating to sudo for the+# workbench.html patch (see inject_browser_scripts in cli/src/main.rs).+"$BIN_DST" install++# Tell the (possibly still-running OLD) :8821 server that an update is staged,+# so the HD frontend can offer "reload when convenient" and auto-reload when+# no Claude sessions are active. Best-effort: pre-1.1.10 servers 404 this.+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+# >>> 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" ] && chmod 0755 "$w" && ln -sfn "$w" "${HOME}/.local/bin/$(basename "$w")"; done+# <<< hydrogen-widget
package.jsonadded+77@@ -0,0 +1,77 @@+{+ "slug": "adom-vscode-macos",+ "version": "1.1.112",+ "type": "app",+ "description": "macOS Hydrogen line of adom-vscode: the same CLI + VS Code extension (same extension id, same :8821 API) built natively for the arm64 Linux workspace Hydrogen runs on a Mac, minus the AI title bar (Hydrogen's agent bar and AI accounts popup own the title bar there). Installs over adom/adom-vscode; Hydrogen keeps this line registered. Upstream: adom-inc/adom-vscode.",+ "license": "PROPRIETARY",+ "tags": [+ "automation",+ "claude",+ "cli",+ "code-server",+ "docker",+ "exec",+ "hydrogen-desktop",+ "hydrogen-macos",+ "ide",+ "macos",+ "queue",+ "settings",+ "vscode"+ ],+ "dependencies": {+ "adom/adom-theme": "^2.0.0"+ },+ "scripts": {+ "install": "./install.sh",+ "uninstall": "./uninstall.sh"+ },+ "needs_sudo": false,+ "visibility": "public",+ "files": [+ "README.md",+ "SKILL.md",+ "LICENSE",+ "install.sh",+ "uninstall.sh",+ "bin/**",+ "screenshots/**",+ "skills/**",+ "VERSION"+ ],+ "confirmed_user_skills": [+ "adom-vscode",+ "adom-vscode-settings",+ "adom-vscode-exec",+ "adom-vscode-queue",+ "adom-vscode-container",+ "adom-vscode-extensions",+ "adom-vscode-modes"+ ],+ "hero": {+ "type": "image",+ "path": "screenshots/hero.png"+ },+ "discovery_triggers": [+ "adom-vscode on mac",+ "vs code editor api on macos",+ "control my editor from the agent",+ "open a file in vs code from the cli",+ "adom-vscode macos"+ ],+ "discovery_pitch": "The macOS line of adom-vscode: the editor API and CLI your AIs use to drive VS Code inside Hydrogen, built natively for arm64, without the AI title bar.",+ "sample_prompts": [+ {+ "label": "Open a file",+ "prompt": "Open src/main.rs in the editor at line 120 with adom-vscode"+ },+ {+ "label": "Run a command",+ "prompt": "Use adom-vscode to toggle the terminal panel"+ },+ {+ "label": "Check the editor",+ "prompt": "Is the adom-vscode editor API up on :8821? Show its version"+ }+ ]+}
screenshots/hero.pngadded⋯ 1 unchanged line ⋯
skills/adom-vscode-container/SKILL.mdadded+88@@ -0,0 +1,88 @@+---+name: adom-vscode-container+description: "Identify the Adom workspace (hd = local Hydrogen workspace — WSL2 on Windows, the systemd-nspawn workspace machine on macOS; hw = Adom cloud container), check and inject the Adom api key at /var/run/adom/api-key, discover the extension's real port when 8821 is taken, and feature-detect verbs via GET /health. Trigger words: container type, hd or hw, what kind of container, am I in hydrogen desktop, cloud container, container identity, api key status, is the api key good, refresh api key, inject api key, /var/run/adom/api-key, session key expiring, port discovery, port.json, ADOM_VSCODE_PORT, health verbs, feature detect, carbon url, vscode proxy uri."+---++Parent skill: **adom-vscode**++# adom-vscode-container, identity + api key + discovery++## Container identity++```bash+adom-vscode container+```++Returns `kind` plus the raw evidence so callers can gate precisely:++- `kind: "hd"`: local Hydrogen workspace — the union of the per-platform markers:+ - **Windows** (WSL2): `/etc/profile.d/hd-env.sh` exists (`markers.hdEnvSh`), or a+ WSL kernel — `microsoft`/`wsl` in `uname -r` (`markers.wslKernel`).+ - **macOS** (Lima VM + systemd-nspawn machine): `/etc/profile.d/hydrogen-env.sh`+ exists (`markers.hydrogenEnvSh`), or `ADOM_HYDROGEN_MACHINE` is set in the+ extension-host env (`markers.hydrogenMachine`). Neither Windows marker is present+ there — the kernel is a plain generic Linux kernel — which is why the machine used+ to report `kind:"unknown"` and refuse exec (fixed 2026-09-17).+- `kind: "hw"`: Adom cloud container. Marker: `VSCODE_PROXY_URI` contains `.adom.cloud`+ (`markers.cloudProxy`). The two macOS markers are ignored when this one is set, so a+ cloud container can never be widened into `hd` by an env var.+- `kind: "unknown"`: none of the markers matched (report the markers, don't guess).++Also included: `hostname`, `os` (from /etc/os-release), `kernel`, `arch`,+`workspaceUser`, and `env.carbonUrl` (`ADOM_CARBON_URL`), `env.hydrogenUrl`,+`env.vscodeProxyUri`, plus `env.hydrogenMachine` (`ADOM_HYDROGEN_MACHINE`), the `markers` object itself and `execAllowed` (whether the exec verbs are enabled here; they are local-workspace only, `kind:hd`). HTTP: `GET /container`.++## API key (/var/run/adom/api-key)++```bash+adom-vscode apikey status # exists, readable, size, mtime, ageSeconds, sha256_8+adom-vscode apikey set --stdin < new-key.txt+adom-vscode apikey set <value>+```++- `status` never prints the key itself, only a sha256 fingerprint (first 8 hex),+ so it is safe to echo into logs and chat.+- `set` writes through passwordless sudo (the path is root-owned), creating+ `/var/run/adom` if needed, mode 644, trailing newline normalized. Use it when+ the frontend completes a login refresh and needs the container to pick up the+ new session key.+- Typical flow with the queue (adom-vscode ships NO built-in watcher; the+ polling process and its cadence belong to HD or another owner): that process+ sees the key aging, pushes a `login/refresh-needed` event (see+ **adom-vscode-queue**); frontend refreshes the session and calls `apikey+ set`; the process confirms via `apikey status` (fresh mtime + new+ fingerprint).++HTTP: `GET /apikey`, `POST /apikey {value}`.++## Port discovery (when 8821 is not the port)++The extension prefers **127.0.0.1:8821**. If busy it walks 8822..8831, then an+ephemeral port, and writes the truth to:++```+~/.local/share/adom-vscode/port.json+# { "port": N, "preferredPort": 8821, "pid": ..., "version": ..., "startedAt": ... }+```++- The CLI resolves automatically: `ADOM_VSCODE_PORT` env var > port.json > 8821.+- External services expecting 8821 should read port.json when 8821 refuses.+- This is not hypothetical: on the macOS workspace machine the live extension has been+ on **8822** (`preferredPort: 8821`) since a detached host took 8821. Read `port.json`;+ never hard-code 8821. Anything that silences or forwards the port (a+ `remote.portsAttributes` entry, a port-hint registration) must cover the whole+ 8821-8831 fallback range, not just 8821.+- The extension also drops an `adom-ports claim` breadcrumb when that CLI exists.++## Feature detection++```bash+adom-vscode health+```++`GET /health` returns `{version, port, preferredPort, portFile, verbs: [...]}`.+Gate on the `verbs` array (e.g. `exec.stream`, `queue.push`, `apikey.set`), not+on version strings. Note: `exec` / `exec.stream` only appear on HD-local+containers; on cloud (hw) containers they are disabled by security policy and+absent from the roster (`execAllowed: false`). An extension that answers /health without a `verbs` field+predates 1.1.10 and needs a window reload or update.
skills/adom-vscode-exec/SKILL.mdadded+80@@ -0,0 +1,80 @@+---+name: adom-vscode-exec+description: "Run shell commands inside the Adom container through the adom-vscode extension, streamed live (SSE) or buffered, as the workspace user via a login shell. LOCAL-WORKSPACE ONLY (kind:hd — WSL2 on Windows, the Hydrogen workspace machine on macOS): cloud (hw) containers refuse exec (exec_disabled_on_cloud), check /health verbs first. THE native replacement for routing workspace commands through an out-of-band host spawn (wsl.exe on Windows, limactl/nsenter on macOS). Trigger words: exec disabled, exec_disabled_on_cloud, exec in container, run command in container, adom-vscode exec, stream exec, streamed output, exec stream, container shell, run npm test in container, long running command, exec timeout, exec cwd, no wsl.exe, wsl exec replacement, POST /exec, SSE exec."+---++Parent skill: **adom-vscode**++# adom-vscode-exec, run container commands natively++> **LOCAL-WORKSPACE ONLY (security policy).** exec runs only where `container`+> reports `kind: hd` — the WSL2 workspace on **Windows** and the systemd-nspawn+> workspace machine on **macOS**. Cloud (hw) containers refuse both exec verbs+> with `errorCode: exec_disabled_on_cloud` (HTTP 403), and /health omits `exec` /+> `exec.stream` from its verb roster there, so feature-detect before offering+> exec. Rationale: adom-vscode is a core install for every Adom user, and an+> arbitrary-shell verb on an internet-facing cloud container is an unnecessary+> exposure; local workspaces sit behind the user's own firewall. On a cloud+> container, run commands in your own terminal/session instead. Debug escape+> hatch: `ADOM_VSCODE_ALLOW_EXEC=1` set in the EXTENSION HOST env by the platform+> (a remote caller cannot set it).+>+> The 403 body carries its own `error`, `kind` and `hint` — read those. It does+> NOT mean the extension is missing, so do not answer a 403 by reinstalling and+> reloading the window (that kills live AI sessions and cannot fix a policy gate).++Commands run as the workspace user (`adom`) via `bash -lc` inside the container,+spawned by the extension host itself. LOGIN shell matters: tools like+adom-bridge need the workspace env from `/etc/profile.d/` (`hd-env.sh` on+Windows, `hydrogen-env.sh` on macOS), and a bare `-c` shell runs them env-less+(they fail with nothing on stdout).++## CLI++```bash+adom-vscode exec "ls -la" --cwd /home/adom/project # streams stdout/stderr live+adom-vscode exec "npm test" --timeout 600 # long jobs: raise the timeout (seconds)+adom-vscode exec "uname -a" --json # buffered: waits, prints one JSON result+```++- Streaming is the default: stdout goes to your stdout, stderr to stderr, and+ the CLI **exits with the remote command's exit code**, so `&&` chains work.+- `--json` (buffered) returns `{ok, exitCode, timedOut, stdout, stderr}` with+ output capped at 64 KB per stream; use streaming for anything chatty.+- Timeouts: streaming default 300s (max 3600), buffered default 120s (max 1800).+ A timed-out command is SIGKILLed and reported (`timedOut: true`, exit 124 on+ the CLI when the remote code is unknown).++## HTTP++- `POST /exec {command, cwd?, timeoutSec?}` buffered.+- `POST /fs/write {path, base64 | text, mkdir?}` writes a file as the workspace user (atomic, parent directories created); the way to land bytes in the container without piping through a shell. Same HD-local policy as exec; `fs.write` appears in `/health` verbs when allowed.+- `GET /stats` returns kernel counters (memory, CPU ticks, OOM-kill count, load, uptime) in one read, for meters and health without a probe.+- `POST /exec/stream {command, cwd?, timeoutSec?}` Server-Sent Events:+ `start`, then `stdout` / `stderr` events (`data` is a JSON-encoded chunk),+ then `exit {exitCode, timedOut}`. Closing the connection kills the child, so+ a frontend that navigates away does not leak processes.++## Why not an out-of-band host spawn++**Windows:** Hydrogen's legacy path spawned `wsl.exe -d Adom-Workspace -u adom --+bash -lc <cmd>` on the Windows host behind a global serial lock. That path caused+nearly every "wsl.exe is flaky / it wedged" incident: E_UNEXPECTED,+management-plane wedges, and everything queued behind one lock, with no VS Code+API access.++**macOS:** the equivalent is `limactl shell` / `nsenter` into the nspawn machine,+or Hydrogen control's `POST 127.0.0.1:47084/workspace/exec` — same story: another+process boundary, no VS Code API, and a per-call setup cost.++Either way: once the editor is up, prefer extension exec for EVERYTHING (on the+extension's real port — see `port.json`, not a hard-coded 8821). Keep the host+spawn only for the bootstrap phase (machine import, code-server start) before+this API exists.++## Gotchas++- The exec verbs are for CONTAINER work. To run a VS Code command (palette+ action), use `adom-vscode command <id>` instead.+- `ok:true` on the buffered form means the process ran; check `exitCode`.+- Feature-detect with `GET /health`: `verbs` contains `exec` and `exec.stream`.
skills/adom-vscode-extensions/SKILL.mdadded+121@@ -0,0 +1,121 @@+---+name: adom-vscode-extensions+description: "Query and manage VS Code extensions in the Adom container: list installed with versions and running state, get one extension's detail plus whether a newer gallery version exists, update one or all, search, install. Includes the AI assistant summary (claude = anthropic.claude-code, codex = openai.chatgpt, kimi = moonshot-ai.kimi-code). Trigger words: is claude installed, claude extension version, codex extension, kimi extension, list extensions, extension version, update extension, extension update pending, install extension, search extensions, extension running, extension state, workbench extensions, open-vsx latest, update claude code extension."+---++Parent skill: **adom-vscode**++# adom-vscode-extensions, query and update extensions++## List and query++```bash+adom-vscode extensions list # non-builtin extensions + the AI summary+adom-vscode extensions list --all # include VS Code builtins+adom-vscode extensions status anthropic.claude-code # version + active state+adom-vscode extensions status anthropic.claude-code --latest # + gallery check+```++- `list` returns `{count, ai, extensions}`. Each entry: `{id, displayName,+ version, active, builtin, path}`. `active` is whether the extension host has+ activated it (running), not merely installed.+- The `ai` object maps the AI assistants directly, `null` when absent: claude+ `anthropic.claude-code`, codex `openai.chatgpt`, kimi `moonshot-ai.kimi-code`,+ antigravity `google.google-antigravity`. `GET /ai/providers` is the authoritative+ list (it also reports each provider's command, surfaces and CLI).+- `--latest` queries the Open VSX gallery (code-server's registry) and returns+ `latestVersion` + `updateAvailable`. Best-effort with a 3s cap: `latestVersion:+ null` means the gallery was unreachable or the extension is not published+ there, NOT that it is current.++HTTP: `GET /extensions`, `GET /extensions?all=1`, `GET /extensions?id=<id>&latest=1`.++## Update++```bash+adom-vscode extensions update anthropic.claude-code # one extension+adom-vscode extensions update # all extensions+```++Updates download in the background; the NEW version only activates on a window+reload. So the honest report to a user is "update requested, activates on+reload", and `extensions status <id>` keeps showing the running (old) version+until then. Warn before reloading: `adom-vscode reload` kills active Claude+Code sessions.++HTTP: `POST /extensions/update {id?}` (omit `id` for all).++## Search and install++```bash+adom-vscode extensions search "python" # opens the Extensions sidebar with the query+adom-vscode extensions install ms-python.python # install by marketplace id+```++HTTP: `POST /extensions/search {query}`, `POST /extensions/install {id}`.++## AI thread icons in the window title bar (1.1.26)++The top-right row of the window (next to VS Code's layout controls) carries one icon per+INSTALLED AI, in the vendor's own colour: Claude Code (orange), Codex, and Kimi or Antigravity+when their extension is present. A "+" badge means a new thread in a tab; a ">_" badge means+the AI's CLI in a terminal tab (TUI, the tab carries the same vendor mark as the launcher: pass+`icon: "adom:claude|codex|kimi"` to POST /terminal/tui to get it on any terminal tab). The plus-with-chevron icon opens a VS Code-style menu: every+installed AI, its TUI, and a checklist (check marks, like the activity bar's own menu) of which+icons to show, saved as the user setting `adom-vscode.aiIcons` (default: every installed AI,+no TUIs). The same menu hides the AI extensions' own+buttons in every tab strip ("Claude Code: Open", "Open Codex Sidebar") by default, because they+duplicate the launcher; its "More" rows keep what they did (Claude's last session, Claude in the side+bar, the Codex side bar) and its last section brings the buttons back (setting+`adom-vscode.hideVendorTabButtons`). The menu closes on a click anywhere, on Escape, and when the+window loses focus (a click into a webview or into Hydrogen's shell). VS Code has no extension contribution point for that row, so the icons are drawn by+`browser/ai-titlebar.js`, which `adom-vscode install` writes into code-server's workbench.html+(present on every reload; a code-server upgrade removes it and the next install puts it back).++Every Hydrogen VS Code tab or panel is its own window with its own extension host (ports+8821, 8822, ...). The script finds ITS window by matching the tabs it can see against each+candidate's `/tabs` census, so a click opens the thread in that window's active group, and it+moves the tab back if the AI extension split the editor.++For callers:++```bash+adom-vscode ai providers # installed? command? icon shown?+adom-vscode ai new '{"type":"codex"}' # a Codex thread in the active group+adom-vscode ai new '{"type":"claude","group":2}' # in editor group 2 (1-based)+adom-vscode ai new '{"type":"claude","tui":true}' # the Claude CLI in a terminal tab+adom-vscode ai icons '{"icons":["claude","codex"]}' # the checklist+adom-vscode tabs close '{"label":"Claude Code"}' # clean up after a test (exact label, or kind)+adom-vscode tabs reload # close + reopen the active (else first) Codex tab+adom-vscode tabs reload '{"resource_contains":"local/<threadId>"}' # one conversation by resource+```++**Package updates from the editor (1.1.99).** The Adom sidebar's Packages section shows the+countdown to the next background package check (adom/hook's cron runs on the hour and the half+hour) and the last run's time and verdict. Its Update now button, the palette entry "Update Adom+packages now", or `POST /packages/update` runs `adom-wiki pkg update` in a terminal named "Adom packages"+so the whole stream is visible; nothing is captured or parsed. An updated adom-vscode activates+only on the next window reload: `adom-vscode install` now asks the running host (`POST /notify`+with `actions: [{label, command}]`) to show a toast with a **Reload now** button, and until the+reload the old version keeps running.++**"Codex could not start its user interface"**: reload that tab, nothing else (John 2026-09-18:+"just reload the tab seems to do the trick"). `POST /tabs/reload` (the verb above) closes the+Codex tab and reopens the same conversation in the same column; the user has the same action on+the tab's right-click menu as **Reload Codex tab** (1.1.94). Nothing polls for the error: the menu+entry is declarative and the verb runs only when called, so never add a watcher around it.++`POST /ai/new` answers `{ok, type, command, tab_seen, label, group, placed, ms}`; `placed` is+`stayed` or `moved a -> b`. Antigravity answers `surface: "tab"` (its extension's editor-panel+mode, a new conversation in the tab while the side bar keeps its own; two conversations is the+ceiling) or, with `surface:"sidebar"` in the body, moves it back to the side bar. `/ai/providers`+lists `surfaces` per AI. An AI that is not installed answers `ok:false` with the provider+list, so never guess: read `installed`. Hydrogen's `/ai-threads/new` remains the route for+anything that also wants tags, names, the AI picker, or a run-prompt job.++## Gotchas++- An extension can be installed but `active: false` (activates lazily). For the+ AI assistants, active = the assistant's panel/agent is actually live.+- Version directories accumulate on disk (`~/.local/share/code-server/extensions/`);+ the `version` reported by these verbs is the RUNNING one, trust it over `ls`.
skills/adom-vscode-queue/SKILL.mdadded+57@@ -0,0 +1,57 @@+---+name: adom-vscode-queue+description: "The shared event queue between container processes and the Hydrogen Desktop / Hydrogen Web frontend, hosted by the adom-vscode extension. Any process pushes events (login refresh needed, long-job progress, arbitrary payloads); the frontend pulls when it wants. Survives window reloads. Trigger words: event queue, queue push, queue pull, push event to frontend, notify hd frontend, frontend event, progress dialog event, api key expiring event, login refresh event, queue status, queue peek, queue topic, adom-vscode queue, POST /queue/push, pull events."+---++Parent skill: **adom-vscode**++# adom-vscode-queue, container-to-frontend events++A simple durable FIFO inside the extension. Producers are anything in the+container (a watcher, a build, an AI thread); the consumer is usually the HD/HW+frontend polling when it cares. Events survive VS Code window reloads+(persisted to `~/.local/share/adom-vscode/queue.json`), are capped at 1000+items, and can carry a per-item TTL.++## Verbs++```bash+adom-vscode queue push login --type refresh-needed --payload '{"expiresInHours":24}'+adom-vscode queue push progress --type job-progress --payload '{"job":"bake","pct":40}' --ttl 120+adom-vscode queue pull --topic login # consume matching events (removes them)+adom-vscode queue pull --peek # read WITHOUT consuming+adom-vscode queue pull --max 10 # cap a batch+adom-vscode queue status # depth + per-topic counts+adom-vscode queue clear --topic progress # drop one topic (or everything without --topic)+```++HTTP: `POST /queue/push {topic?, type?, payload?, ttlSec?}` returns `{id, depth}`;+`GET/POST /queue/pull {topic?, max?, peek?}` returns `{items, count, remaining}`;+`GET /queue/status`; `POST /queue/clear {topic?}`.++Each item: `{id, ts, topic, type, payload, ttlSec}`. Default topic is+`default`, default type is `event`. Expired items vanish on the next access.++## Patterns++- **Login refresh**: NOTE, adom-vscode ships NO built-in key watcher; this is+ the pattern for whichever process owns key freshness (an HD daemon, a cron+ job, any container process). That process decides its own cadence, notices+ the session key in `/var/run/adom/api-key` is close to expiry, and pushes+ `topic: login, type: refresh-needed`; the frontend pulls, shows the "needs a+ login refresh" banner, refreshes, then injects the new key (see+ **adom-vscode-container** for `apikey set`).+- **Progress dialog**: a long container job pushes `topic: progress` events with+ a TTL a bit above the push interval, so stale progress self-cleans if the+ frontend never pulls.+- **Peek before consume**: a frontend that only renders a badge should+ `--peek`; consume (`pull` without peek) only when the event is handled, so a+ crashed frontend does not eat events.++## Semantics worth knowing++- Pull REMOVES what it returns (unless `peek`). Two consumers splitting one+ topic will race; give each its own topic.+- Depth is bounded at 1000: oldest items drop first past the cap.+- The queue is per-container and in one extension host; a window reload reloads+ it from disk, so nothing is lost across reloads.
skills/adom-vscode-settings/SKILL.mdadded+90@@ -0,0 +1,90 @@+---+name: adom-vscode-settings+description: "Get/set ANY VS Code setting, the color theme, fonts, and font sizes LIVE from inside the container, with no window reload, so active Claude/AI sessions survive. Uses the native VS Code API (ConfigurationTarget.Global), the fix for settings.json merges that hang or need a workbench reload. Also reports the RESOLVED font family the renderer truly shows (document.fonts.check), not just the configured stack. Trigger words: change theme, set theme, vscode theme, dark theme, set font, change font, font size, editor font, terminal font size, chat font, prose font, vscode setting, config get, config set, workbench.colorTheme, editor.fontFamily, editor.fontSize, chat.fontFamily, live settings, no reload, which font is active, resolved font, font not applying, theme without reload."+---++Parent skill: **adom-vscode**++# adom-vscode-settings, live config / theme / fonts++Every verb here applies through `vscode.workspace.getConfiguration().update(key,+value, ConfigurationTarget.Global)`, which lands **immediately**: no window+reload, no settings.json file merge, and active Claude Code sessions survive.+Prefer these over editing `~/.local/share/code-server/User/settings.json` by+hand, and NEVER route a settings change through an out-of-band host spawn+(`wsl.exe` on Windows, `limactl`/`nsenter` or `POST 127.0.0.1:47084/workspace/exec`+on macOS).++## Any setting++```bash+adom-vscode config get workbench.colorTheme # effective value + default/global/workspace layers+adom-vscode config set editor.fontSize 14 # values parse as JSON when possible, else string+adom-vscode config set workbench.statusBar.visible false+```++HTTP: `GET /config?key=<dotted.key>`, `POST /config {key, value}`.++## Theme++```bash+adom-vscode theme get # active + preferredDark/preferredLight + EVERY installed theme+adom-vscode theme set "Adom Studio Dark" # switches live. Installed Adom labels:+ # Adom Studio, Adom Studio Dark,+ # Adom Studio Dark (Brighter Text),+ # Adom Kickstand, Adom Slate.+ # An unknown label answers known:false and+ # leaves the editor on the default (white) theme —+ # always read `theme get` for the real list first.+```++`theme get` enumerates every theme contributed by installed extensions+(label, uiTheme, owning extension), so you can offer the user a real list.+`theme set` warns (but still applies) if the name is not among installed+themes; a wrong name silently falls back to the default look, so check the+`available` list when a set "didn't take".++HTTP: `GET /theme`, `POST /theme {theme}`.++## Fonts++```bash+adom-vscode font get # all font settings + RESOLVED (truly rendered) families+adom-vscode font set --editor-family "Fira Code" --prose-family Inter --editor-size 14+adom-vscode font-size get+adom-vscode font-size set --editor 14 --terminal 13 --prose 15 --prose-code 13+```++The key map (CLI flag -> settings key):++| Flag | Key | What it styles |+|---|---|---|+| `--editor-family` | `editor.fontFamily` | Code editor |+| `--editor-size` | `editor.fontSize` | Code editor |+| `--terminal-size` | `terminal.integrated.fontSize` | Integrated terminal |+| `--prose-family` | `chat.fontFamily` | Chat prose (Claude panel text) |+| `--prose-size` | `chat.fontSize` | Chat prose |+| `--prose-code-family` | `chat.editor.fontFamily` | Chat code blocks |+| `--prose-code-size` | `chat.editor.fontSize` | Chat code blocks |++HTTP: `GET /fonts`, `POST /fonts {editorFamily?, proseFamily?, proseCodeFamily?,+editorSize?, terminalSize?, proseSize?, proseCodeSize?}` (any subset).++## Resolved fonts, the part HD needs++The configured stack often names fonts that are NOT installed on the rendering+side (the user's browser or the HD webview), so reading the setting alone lies.+`font get` also returns `resolved`: the first family in each stack that the+renderer can truly draw, checked with `document.fonts.check` inside the+workbench (via the Adom sidebar webview). If the sidebar webview is not alive+the response says so (`resolved.via: null` plus a note) and only configured+values are returned; the check works again once the Adom sidebar has been opened.++## Gotchas++- Font/theme changes by these verbs need NO reload. If a user reports a settings+ change "didn't apply", suspect the old host-spawn/settings.json path or a wrong+ key name, not these verbs.+- `config set` writes the Global (User) layer. A workspace-layer override+ (`.vscode/settings.json`) still wins; `config get` shows all layers so you can+ see when that is happening.
uninstall.shadded+30@@ -0,0 +1,30 @@+#!/usr/bin/env bash+# adom-wiki pkg uninstall script for adom-vscode.++set -euo pipefail++USER_BIN="${HOME}/.local/bin/adom-vscode"+LEGACY_BIN="/usr/local/bin/adom-vscode"++if [ -f "$USER_BIN" ]; then+ rm -f "$USER_BIN"+ echo "Removed $USER_BIN"+fi++if [ -f "$LEGACY_BIN" ]; then+ # The pre-needs_sudo:false versions of adom-vscode installed into+ # /usr/local/bin. Clean that up too (best-effort; sudo may not be+ # available in daemon contexts — that's fine, the user-local copy+ # is gone so the daemon won't manage it anymore).+ if command -v sudo >/dev/null 2>&1; then+ sudo rm -f "$LEGACY_BIN" 2>/dev/null || true+ fi+fi++COMP="${HOME}/.local/share/bash-completion/completions/adom-vscode"+[ -f "$COMP" ] && rm -f "$COMP"++for s in adom-vscode adom-vscode-settings adom-vscode-exec adom-vscode-queue adom-vscode-container adom-vscode-extensions adom-vscode-modes; do+ rm -rf "$HOME/.claude/skills/$s"+done+echo "Removed the adom-vscode skill pack."