BundledSoftware DevelopmentVersion 1.0.0

Inspect the Live Hermes Desktop DOM Over CDP

Read the live Hermes desktop DOM/CSS over CDP.

Written by Neura Market from the official Hermes Agent documentation for Inspecting Hermes Desktop Dom. Commands, paths, and version numbers are reproduced from the source unchanged.

Read the official documentation

When you are working on apps/desktop and the user has the same app running, you can read the rendered DOM of the window they are looking at instead of guessing from .tsx files. This skill covers the Chrome DevTools Protocol port that dev-server runs expose, the one-liner and shared client for querying it, and the rules that keep you from disturbing the user's session. Reach for it when a style question needs a factual answer: computed values, geometry, which rule won, or what the renderer logged.

What it does

The desktop renderer is a Chromium page, so anything DevTools can inspect, a script can inspect. Dev-server runs open a CDP port on 127.0.0.1:9222 automatically, and this skill documents how to connect to it and ask precise questions about the live tree: computed styles, element geometry, matching selectors, and console output. The point is to stop inferring behaviour from source and start reading what actually rendered.

It answers factual questions only. Whether the result looks good, whether the spacing feels right, whether a colour balance works: those still need the user's eyes or a screenshot. Use CDP for facts and hand aesthetics back to the user.

Before you start

The port is open on 127.0.0.1:9222 for any dev-server run. It is closed in exactly two cases, both defined in apps/desktop/electron/dev-cdp.ts:

  • Packaged builds. Always closed, and no environment value overrides it.
  • No HERMES_DESKTOP_DEV_SERVER. An unpackaged electron . against dist/ is how the packaged app gets smoke tested, so it behaves like one.

HERMES_DESKTOP_CDP_PORT moves the port (=9333) or disables it (=off).

Check the port before doing anything else:

curl -s --max-time 3 http://127.0.0.1:${HERMES_DESKTOP_CDP_PORT:-9222}/json/version

An empty response means no port. Do not silently guess another port.

Never relaunch the user's app to get a port. That destroys their session and their state. If you need an instance of your own, launch an isolated one as described below.

Reading the DOM

apps/desktop/scripts/eval.mjs is the one-liner for a single expression:

cd apps/desktop
node scripts/eval.mjs "document.querySelectorAll('[data-slot]').length"

For multi-step work, use the shared client. It handles target discovery and promise-aware evaluation:

import { CDP, SELECTORS } from './scripts/perf/lib/cdp.mjs'

const cdp = await CDP.connect({ port: 9222, match: '5174' })
const out = await cdp.eval(`JSON.stringify({
  radius: getComputedStyle(document.documentElement).getPropertyValue('--radius-scalar').trim(),
  composer: !!document.querySelector('[data-slot="composer-rich-input"]')
})`)
cdp.close()

SELECTORS in scripts/perf/lib/cdp.mjs holds the stable data-slot hooks: composer, thread viewport, assistant message, turn pair, profile rail. Prefer these over inventing a querySelector. They are updated as a unit when components move, so they stay accurate in a way hand-written selectors will not.

The question this is best at: which rule won?

Editing every call site because a style "isn't applying" is the classic waste. Read the real node first:

const el = document.querySelector('[data-slot="aui_assistant-message-root"] a')
JSON.stringify({
  ownClasses: el.className,
  weight: getComputedStyle(el).fontWeight,
  parents: (() => {
    const out = []
    let n = el
    while ((n = n.parentElement) && out.length < 6) out.push(n.className)
    return out
  })()
})

If the node carries no class of its own, the value is inherited. Sweeping call sites will not fix it; you need the ancestor rule. A plugin stylesheet such as @tailwindcss/typography's prose a { font-weight: 500 } routinely beats a utility class, so override on the shared class rather than at each usage.

Your own isolated instance

When there is no port, or you must not disturb the user's window, launch a separate instance:

cd apps/desktop
HERMES_HOME=$HOME/.hermes/cache/scratch/cdp-probe-home \
HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 \
HERMES_DESKTOP_CDP_PORT=9333 \
  npx electron . --user-data-dir=$HOME/.hermes/cache/scratch/cdp-probe-userdata

The separate --user-data-dir dodges Electron's single-instance lock, so it cannot collide with a running hgui. The separate HERMES_HOME keeps it away from real sessions. Pick a port other than 9222 for the same reason. Run it in the background and kill it when done.

npm run perf:serve does the same with a temp HERMES_HOME baked in, if you also want the perf harness.

When not to use it

This skill is not for performance profiling or heap work. Those belong to node-inspect-debugger and debugging-hermes-desktop. It is also the wrong tool when the real question is "does this look right", because CDP cannot answer that.

Limits and gotchas

  • Never kill the user's dev server or app to "free" anything. A mid-serve kill nukes Chromium's socket pool, and the resulting ERR_NETWORK_CHANGED gets blamed on whatever you just changed.
  • A throwaway HERMES_HOME has no backend. The app logs ECONNREFUSED for hermes:api and may exit on its own. The renderer still mounts and the DOM is readable, so read promptly, and do not mistake a self-exited probe for a broken port. Chromium logs DevTools listening on ws://127.0.0.1:/… when it binds; that line is the proof the port opened.
  • Poll, don't probe once. A just-launched app needs a second or two before the port answers.
  • Never dump the whole DOM. The desktop renders hundreds of nodes and outerHTML will bury your context. Project down to a small JSON object inside the evaluated expression.
  • Pass match to CDP.connect. Without it you may attach to the pet overlay, quick-entry window, or a devtools target instead of the main window.
  • cdp.eval returns the value; raw Runtime.evaluate double-nests it (.result.result.value). Use the wrapper.
  • import.meta.env.DEV is true under vite dev in this repo. The note in apps/desktop/scripts/profile-typing-lag.md claiming otherwise is stale.

What pairs with it

For performance and heap questions, use node-inspect-debugger. For a general method of tracking down a bug across layers, systematic-debugging gives you the discipline to frame the question before you query the DOM. dogfood is the companion when you want to exercise the app as a user rather than inspect it as a developer.

Skills the docs pair this with

More Software Development skills