Hydrogen Desktop Bootstrap (apps + skills)
Public Made by Adomby adom
Layer Hydrogen Desktop's workspace onto a standard Adom workstation.
name: hd-eval description: > Inject JavaScript into ANY surface of the Hydrogen Desktop window from inside the workspace — the app shell (top nav, dialogs), the VS Code workbench, the wiki panel, or the Claude chat webview. Backed by the control API's POST /eval-in, which connects directly to the right CDP target and returns the JS value. Use it to build add-ons (e.g. a usage meter in the top bar), demo overlays (a moving arrow that points at things), highlight elements, read DOM state, or drive the UI. Works in dev AND release builds. Trigger words: eval-in, inject ui, eval into hd, hd overlay, demo arrow, highlight element, add-on, addon, usage meter, inject script, target shell, target workbench, target claude, find probe, eval-in targets, point at element, nav widget.
hd-eval — inject JS into any HD surface (POST /eval-in)
The HD window is not one page. It's a tree of cross-origin surfaces, each a separate Chrome DevTools Protocol (CDP) target/context:
| friendly target | what it is | typical use |
|---|---|---|
shell |
the HD app shell (Svelte): top nav, logo menu, dialogs, the floating layer | inject add-ons/overlays that float over everything |
workbench |
the VS Code editor (code-server) | activity bar, tabs, the "Claude Code: Open" button |
wiki |
the wiki panel iframe | wiki DOM |
claude |
the Claude Code chat webview (only exists while the panel is open) | the chat input + send button |
/eval-in figures out where a surface lives (it differs between dev and release —
see "Why two paths" below) and runs your JS there, returning the value.
Finding the control port + reaching it from the workspace
The control API port is dynamic (auto-resolves conflicts per launch). From inside
the workspace, reach HD on 127.0.0.1 (WSL2 mirrored networking shares loopback with the
Windows host). Read the live control URL from the discovery file ~/.adom/hd-control-url
(your non-interactive Bash shells don't source .bashrc/profile.d, so the env var alone
is unreliable):
BASE="$(cat ~/.adom/hd-control-url)" # http://127.0.0.1:<dynamic>
(Never hardcode the port — read the live URL from ~/.adom/hd-control-url.)
Discover the surfaces
curl -sf $BASE/targets | jq .
# each surface has a friendly alias (shell|workbench|wiki|claude), url, id.
# the claude target (Claude chat webview) appears only when that panel is open.
POST /eval-in with no target (or no js) also returns the surface list, so an
agent can ask "what can I inject into?" and pick.
The call
POST /eval-in
{
"target": "shell" | "workbench" | "wiki" | "claude" | "<url-substring>" | "<id>",
"js": "<javascript; value of last expression is returned>",
"find": "<OPTIONAL JS expression>", // disambiguate among same-origin contexts
"frame": "<url-substring | index>", // OPTIONAL nested-context selector
"awaitPromise": true // OPTIONAL (default true)
}
→ { "ok": true, "value": <result>, "target": "<matched url>", "contextId": N, "frames": [...] }
find — the important one
A surface can expose several same-origin nested contexts (the workbench
exposes ~6 localhost:7380 contexts). Origin alone is ambiguous, so pass find —
a JS expression (e.g. document.querySelector('...'), NOT a bare CSS selector).
/eval-in evals it in each candidate context and runs your js in the first
one where find is truthy.
Recipes
Add-on: a widget in the top nav (e.g. a usage meter) — target shell
Inject once; it persists until reload. Append your element to nav in the shell.
Demo overlay: a red arrow over everything that you reposition — target shell
Inject a position:fixed; z-index:2147483647; pointer-events:none element with a
CSS transition into the shell body, then move it with cheap follow-up evals
(it glides). The shell floats over all iframes, so window coords work everywhere.
Highlight an element (even deep in the workbench / Claude webview)
Eval inside the element's own surface and set outline — no coordinate math.
Use find to land in the right context, e.g.
find: document.querySelector("a[aria-label='Claude Code: Open']").
Type into & submit the Claude chat (panel must be open)
Stable selectors (NOT the CSS-module hash classes like .messageInput_xxxx,
which change per build):
- input =
[aria-label='Message input'](acontenteditablediv) — focus it, thendocument.execCommand('insertText', false, text). - submit =
button[type='submit'][data-permission-mode](disabled until text) —.click()it; fallback = dispatch anEnterkeydown on the input.
Why two paths (dev vs release) — you don't have to care
- Dev build: shell is
localhost:1420; the workbench (localhost:7380) is same-process, so it's a context INSIDE the shell page (not a separate target)./eval-infalls back to the shell and filters contexts by origin. - Release build: shell is
tauri.localhost; the workbench is a different site → an out-of-process iframe (OOPIF) → its own top-level target./eval-inconnects directly.
You just pass target:"workbench" (or claude) and /eval-in picks the right
path. Prefer it over the older /eval (shell only) and /iframe-eval
(page-context walk, breaks in release).
Verify-then-fallback (recommended)
/eval-in returns the JS value, so always verify: after an action, eval a probe
and check it; if it fails, try a second technique. The HD "eval-in demo" floating
toolbar does exactly this and logs endpoint + params + verify + fallback per step.
Don'ts
- Don't hardcode the control port — read
/ports. - Don't pass
findas a bare CSS selector — it's a JS expression. - Don't select by CSS-module hash classes — use stable attributes (
aria-label,data-*,role). - Don't expect the
claudewebview to exist unless the Claude panel is open. - Don't use
/evalfor workbench/Claude — it only reaches the shell page.
Cross-references
- hd-api — the full control API surface
- hd-ui / hd-adom-menu — where UI elements live
- hd-self-screenshot — capture the result of your injection
Recipe: hide/show activity-bar icons (the RIGHT way — persisted, overflow-proof)
Do NOT hide activity-bar icons with display:none on .action-item — that misses
icons collapsed into the "…" (Additional Views) overflow and doesn't persist.
Instead write VS Code's own source-of-truth state, then reload the workbench:
- DB:
vscode-web-state-db-global→ storeItemTable→ keyworkbench.activity.pinnedViewlets2 - It's an array of
{id, pinned, visible, order}. Setpinned:falseto hide an icon from the activity bar (works even if it's in the overflow),pinned:trueto restore. - IDs: Explorer
workbench.view.explorer· Searchworkbench.view.search· Source Controlworkbench.view.scm· Run&Debugworkbench.view.debug· Extensionsworkbench.view.extensions(extension views:workbench.view.extension.<id>). - IndexedDB is per-origin, so read/write from any
localhost:7380context (targetworkbench,find: "(typeof indexedDB!=='undefined')") — don't require the workbench main doc (it's absent during a reload). - After writing, reload the workbench (
iframe.src=iframe.srcon theshell, orPOST /reload-vscode) so VS Code re-readspinnedViewlets2.
// hide Search / Source Control / Run&Debug (then reload)
{ "target":"workbench", "find":"(typeof indexedDB!=='undefined')", "awaitPromise":true,
"js":"(async function(){function g(d,k){return new Promise(function(r){var o=indexedDB.open(d);o.onsuccess=function(){var b=o.result;var q=b.transaction('ItemTable','readonly').objectStore('ItemTable').get(k);q.onsuccess=function(){b.close();r(q.result)}}})}function p(d,k,v){return new Promise(function(r){var o=indexedDB.open(d);o.onsuccess=function(){var b=o.result;var t=b.transaction('ItemTable','readwrite');t.objectStore('ItemTable').put(v,k);t.oncomplete=function(){b.close();r(true)}}})}var DB='vscode-web-state-db-global',K='workbench.activity.pinnedViewlets2';var a=JSON.parse(await g(DB,K));var ids=['workbench.view.search','workbench.view.scm','workbench.view.debug'];a.forEach(function(e){if(ids.indexOf(e.id)>-1)e.pinned=false});await p(DB,K,JSON.stringify(a));return 'hidden';})()" }
The HD demo toolbar's Hide / Restore activity icons buttons (/demo/run {action:"hide-activitybar"|"show-activitybar"}) do exactly this. The setup-steps
configure-vscode step should hide icons this way too (persisted across the
mid-setup reload), not via CSS or transient :8821 layout calls.
---
name: hd-eval
description: >
Inject JavaScript into ANY surface of the Hydrogen Desktop window from inside
the workspace — the app shell (top nav, dialogs), the VS Code workbench, the
wiki panel, or the Claude chat webview. Backed by the control API's POST
/eval-in, which connects directly to the right CDP target and returns the JS
value. Use it to build add-ons (e.g. a usage meter in the top bar), demo
overlays (a moving arrow that points at things), highlight elements, read DOM
state, or drive the UI. Works in dev AND release builds. Trigger words:
eval-in, inject ui, eval into hd, hd overlay, demo arrow, highlight element,
add-on, addon, usage meter, inject script, target shell, target workbench,
target claude, find probe, eval-in targets, point at element, nav widget.
---
# hd-eval — inject JS into any HD surface (`POST /eval-in`)
The HD window is **not one page**. It's a tree of cross-origin surfaces, each a
separate Chrome DevTools Protocol (CDP) target/context:
| friendly target | what it is | typical use |
|---|---|---|
| `shell` | the HD app shell (Svelte): top nav, logo menu, dialogs, the floating layer | inject add-ons/overlays that float over everything |
| `workbench` | the VS Code editor (code-server) | activity bar, tabs, the "Claude Code: Open" button |
| `wiki` | the wiki panel iframe | wiki DOM |
| `claude` | the Claude Code chat **webview** (only exists while the panel is open) | the chat input + send button |
`/eval-in` figures out where a surface lives (it differs between dev and release —
see "Why two paths" below) and runs your JS there, returning the value.
## Finding the control port + reaching it from the workspace
The control API port is **dynamic** (auto-resolves conflicts per launch). From inside
the workspace, reach HD on `127.0.0.1` (WSL2 mirrored networking shares loopback with the
Windows host). Read the live control URL from the discovery file `~/.adom/hd-control-url`
(your non-interactive Bash shells don't source `.bashrc`/`profile.d`, so the env var alone
is unreliable):
```bash
BASE="$(cat ~/.adom/hd-control-url)" # http://127.0.0.1:<dynamic>
```
(Never hardcode the port — read the live URL from `~/.adom/hd-control-url`.)
## Discover the surfaces
```bash
curl -sf $BASE/targets | jq .
# each surface has a friendly alias (shell|workbench|wiki|claude), url, id.
# the claude target (Claude chat webview) appears only when that panel is open.
```
`POST /eval-in` with no `target` (or no `js`) also returns the surface list, so an
agent can ask "what can I inject into?" and pick.
## The call
```
POST /eval-in
{
"target": "shell" | "workbench" | "wiki" | "claude" | "<url-substring>" | "<id>",
"js": "<javascript; value of last expression is returned>",
"find": "<OPTIONAL JS expression>", // disambiguate among same-origin contexts
"frame": "<url-substring | index>", // OPTIONAL nested-context selector
"awaitPromise": true // OPTIONAL (default true)
}
→ { "ok": true, "value": <result>, "target": "<matched url>", "contextId": N, "frames": [...] }
```
### `find` — the important one
A surface can expose **several same-origin nested contexts** (the workbench
exposes ~6 `localhost:7380` contexts). Origin alone is ambiguous, so pass `find` —
a JS **expression** (e.g. `document.querySelector('...')`, NOT a bare CSS selector).
`/eval-in` evals it in each candidate context and runs your `js` in the **first
one where `find` is truthy**.
## Recipes
### Add-on: a widget in the top nav (e.g. a usage meter) — target `shell`
Inject once; it persists until reload. Append your element to `nav` in the shell.
### Demo overlay: a red arrow over everything that you reposition — target `shell`
Inject a `position:fixed; z-index:2147483647; pointer-events:none` element with a
CSS `transition` into the shell `body`, then move it with cheap follow-up evals
(it glides). The shell floats over all iframes, so window coords work everywhere.
### Highlight an element (even deep in the workbench / Claude webview)
Eval **inside the element's own surface** and set `outline` — no coordinate math.
Use `find` to land in the right context, e.g.
`find: document.querySelector("a[aria-label='Claude Code: Open']")`.
### Type into & submit the Claude chat (panel must be open)
Stable selectors (NOT the CSS-module hash classes like `.messageInput_xxxx`,
which change per build):
- input = `[aria-label='Message input']` (a `contenteditable` div) — focus it,
then `document.execCommand('insertText', false, text)`.
- submit = `button[type='submit'][data-permission-mode]` (disabled until text) —
`.click()` it; fallback = dispatch an `Enter` keydown on the input.
## Why two paths (dev vs release) — you don't have to care
- **Dev build:** shell is `localhost:1420`; the workbench (`localhost:7380`) is
same-process, so it's a context INSIDE the shell page (not a separate target).
`/eval-in` falls back to the shell and filters contexts by origin.
- **Release build:** shell is `tauri.localhost`; the workbench is a different site →
an out-of-process iframe (OOPIF) → its own top-level target. `/eval-in` connects
directly.
You just pass `target:"workbench"` (or `claude`) and `/eval-in` picks the right
path. Prefer it over the older `/eval` (shell only) and `/iframe-eval`
(page-context walk, breaks in release).
## Verify-then-fallback (recommended)
`/eval-in` returns the JS value, so always verify: after an action, eval a probe
and check it; if it fails, try a second technique. The HD "eval-in demo" floating
toolbar does exactly this and logs endpoint + params + verify + fallback per step.
## Don'ts
- Don't hardcode the control port — read `/ports`.
- Don't pass `find` as a bare CSS selector — it's a JS expression.
- Don't select by CSS-module hash classes — use stable attributes (`aria-label`, `data-*`, `role`).
- Don't expect the `claude` webview to exist unless the Claude panel is open.
- Don't use `/eval` for workbench/Claude — it only reaches the shell page.
## Cross-references
- **hd-api** — the full control API surface
- **hd-ui** / **hd-adom-menu** — where UI elements live
- **hd-self-screenshot** — capture the result of your injection
## Recipe: hide/show activity-bar icons (the RIGHT way — persisted, overflow-proof)
Do NOT hide activity-bar icons with `display:none` on `.action-item` — that misses
icons collapsed into the "…" (Additional Views) **overflow** and doesn't persist.
Instead write VS Code's own source-of-truth state, then reload the workbench:
- **DB:** `vscode-web-state-db-global` → store `ItemTable` → key `workbench.activity.pinnedViewlets2`
- It's an array of `{id, pinned, visible, order}`. Set **`pinned:false` to hide** an
icon from the activity bar (works even if it's in the overflow), **`pinned:true` to restore**.
- **IDs:** Explorer `workbench.view.explorer` · Search `workbench.view.search` ·
Source Control `workbench.view.scm` · Run&Debug `workbench.view.debug` ·
Extensions `workbench.view.extensions` (extension views: `workbench.view.extension.<id>`).
- IndexedDB is **per-origin**, so read/write from **any** `localhost:7380` context
(target `workbench`, `find: "(typeof indexedDB!=='undefined')"`) — don't require the
workbench main doc (it's absent during a reload).
- After writing, **reload the workbench** (`iframe.src=iframe.src` on the `shell`, or
`POST /reload-vscode`) so VS Code re-reads `pinnedViewlets2`.
```jsonc
// hide Search / Source Control / Run&Debug (then reload)
{ "target":"workbench", "find":"(typeof indexedDB!=='undefined')", "awaitPromise":true,
"js":"(async function(){function g(d,k){return new Promise(function(r){var o=indexedDB.open(d);o.onsuccess=function(){var b=o.result;var q=b.transaction('ItemTable','readonly').objectStore('ItemTable').get(k);q.onsuccess=function(){b.close();r(q.result)}}})}function p(d,k,v){return new Promise(function(r){var o=indexedDB.open(d);o.onsuccess=function(){var b=o.result;var t=b.transaction('ItemTable','readwrite');t.objectStore('ItemTable').put(v,k);t.oncomplete=function(){b.close();r(true)}}})}var DB='vscode-web-state-db-global',K='workbench.activity.pinnedViewlets2';var a=JSON.parse(await g(DB,K));var ids=['workbench.view.search','workbench.view.scm','workbench.view.debug'];a.forEach(function(e){if(ids.indexOf(e.id)>-1)e.pinned=false});await p(DB,K,JSON.stringify(a));return 'hidden';})()" }
```
The HD demo toolbar's **Hide / Restore activity icons** buttons (`/demo/run
{action:"hide-activitybar"|"show-activitybar"}`) do exactly this. The setup-steps
`configure-vscode` step should hide icons this way too (persisted across the
mid-setup reload), not via CSS or transient `:8821` layout calls.