Download

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'] (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).

/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.
// 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.