Skip to content

feat(desktop): let the agent inspect the desktop app it's developing - #73121

Merged
OutThisLife merged 3 commits into
mainfrom
bb/desktop-dev-cdp-port
Jul 28, 2026
Merged

feat(desktop): let the agent inspect the desktop app it's developing#73121
OutThisLife merged 3 commits into
mainfrom
bb/desktop-dev-cdp-port

Conversation

@OutThisLife

@OutThisLife OutThisLife commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Hermes develops Hermes Desktop but cannot see it. UI questions — did that change take effect, why is this element still the wrong colour, what does this token compute to — get answered by reading .tsx and inferring, or by asking the user to look. The renderer is a Chromium page; it was one command-line switch away from being readable.

How it works

The switch. app.commandLine.appendSwitch('remote-debugging-port', N) makes Chromium start a debugging server. It has to be set at module scope (main.ts:280) — Chromium parses its command line during init, which happens between module evaluation and app.whenReady() (line 11378). Appending the switch inside the ready handler is a silent no-op. That's why this sits with the GPU/compositing switches rather than anywhere more obvious.

What the port serves. GET /json/list enumerates every debuggable target (each BrowserWindow, plus devtools windows) with a webSocketDebuggerUrl. Open that socket and you're speaking CDP: Runtime.evaluate for expressions, DOM/CSS for structure and computed styles, Input.dispatchKeyEvent for synthetic input. scripts/perf/lib/cdp.mjs already wraps all of it — request/response correlation by id, event listeners, promise-aware eval.

Why 9222. 24 scripts under scripts/ already hardcode or default to it. Matching that default is what makes the toolkit work with zero configuration rather than needing every script touched.

Target selection matters. A running desktop has several page targets — main window, pet overlay (?win=overlay), quick entry (?win=quick). CDP.connect({ match: '5174' }) filters on the dev-server URL to get the main window; without match you attach to whichever target enumerates first.

How the gate works

electron/dev-cdp.ts is a pure function over { env, isPackaged, devServer }. Pure because the alternative is testing a gate by launching Electron, which is slow enough that nobody does it, and the packaged branch is the one that must never regress.

Packaged is checked first, and it's stronger than it looks:

const IS_PACKAGED = app.isPackaged || Boolean(process.env.HERMES_DESKTOP_IS_PACKAGED)

scripts/bundle-electron-main.mjs esbuild-defines process.env.HERMES_DESKTOP_IS_PACKAGED to the literal true for every non---dev bundle. So in a production bundle that expression is constant-folded at build time — the packaged check isn't reading the environment, it's a baked-in true that survives an unpackaged electron . against dist/. Setting the env var can only ever turn the gate on, never off.

The dev-server check is the second discriminator. HERMES_DESKTOP_DEV_SERVER is only set by npm run dev / hgui, so its presence distinguishes a source-tree run from a dist/-loading one. The dist/ case is how the packaged app gets smoke tested, so it should behave like the packaged app.

Everything else opens on 9222. HERMES_DESKTOP_CDP_PORT is an override — a different port, or off/0/false/no. Out-of-range and non-integer values are refused rather than silently coerced (Number('92 22') is NaN, but Number('') is 0, which is why the empty case is handled before the numeric parse).

Refusals log only when they contradict something explicitly requested — a typo'd port, an explicit off. Packaged and dist/ runs are closed by design and stay quiet, because a warning on every packaged launch is noise nobody can act on.

Why on by default

A dev server already executes arbitrary local JS: vite's module graph, every postinstall in node_modules. A loopback CDP port doesn't widen that, and perf:serve has opened one unconditionally since it was written. The address is pinned to 127.0.0.1 explicitly rather than relying on Chromium's default, and isn't configurable — off-host renderer debugging has no use case here, and the knob would only invite someone to try.

The skill

skills/software-development/inspecting-hermes-desktop-dom — the port is useless if the agent doesn't know it exists. Sits beside node-inspect-debugger, which covers the same protocol for Node/perf; this is the DOM/CSS half.

The load-bearing content is what goes wrong:

  • Never relaunch or kill the user's app to get a port. A mid-serve kill nukes Chromium's socket pool; the resulting ERR_NETWORK_CHANGED gets blamed on whatever was edited last. Launch an isolated instance (--user-data-dir dodges the single-instance lock, separate HERMES_HOME keeps it off real sessions).
  • Never dump the whole DOM. Hundreds of nodes; project down to a small JSON object inside the evaluated expression.
  • "Which rule won?" — read getComputedStyle on the real node and walk ancestors before editing call sites. No class on the node means the value is inherited and sweeping usages won't fix it.
  • A throwaway HERMES_HOME has no backend, so a probe instance logs ECONNREFUSED and can exit on its own. Chromium's DevTools listening on ws://… line is the proof the port bound; don't read a self-exited probe as a broken gate. (Hit this during verification.)
  • CDP answers factual questions, not aesthetic ones. Renders-or-not is a fact. Looks-right is the user's call.

Also

scripts/eval.mjs hardcoded :9222 and threw a raw ECONNREFUSED stack when nothing was listening. Honours the override now, falls back to any page target when the dev-port match misses, and explains itself on failure.

Verification

Gate: 11 unit tests (vitest run --project electron electron/dev-cdp.test.ts). Skill frontmatter run through the repo's own _validate_frontmatter + extract_skill_description — first draft truncated at the 57-char index budget and lost its trigger word, so it was shortened; tests/tools/test_skill_manager_tool.py 129 passed.

Runtime, real Electron, isolated --user-data-dir + HERMES_HOME so a live hgui was never touched:

run result
dev server, no env var DevTools listening on ws://127.0.0.1:9222/…; lsof shows TCP 127.0.0.1:9222 (LISTEN) — loopback, not *:9222; scripts/eval.mjs returned {"title":"Hermes","nodes":216}
HERMES_DESKTOP_CDP_PORT=off closed; logged renderer debugging disabled by HERMES_DESKTOP_CDP_PORT.
HERMES_DESKTOP_IS_PACKAGED=1 + =9222 closed, silent — no DevTools listening, nothing bound

tsc --build tsconfig.electron.json and eslint clean. 413 insertions, 2 deletions across 6 files.

The renderer is a Chromium page, and apps/desktop already carries a whole
CDP toolkit for it — scripts/eval.mjs, scripts/perf/lib/cdp.mjs with its
shared SELECTORS map, and the diag-*/probe-* family. None of it can
attach to `hgui` or `npm run dev`, because neither passes
--remote-debugging-port. The only launcher that opens one is
`npm run perf:serve`, which is a separate isolated instance rather than
the app you're looking at.

Add HERMES_DESKTOP_CDP_PORT. When set, the shell opens a CDP port on
loopback so that existing tooling can read the live DOM: computed
styles, geometry, which rule actually won.

Three independent gates, all required, resolved by a pure function in
electron/dev-cdp.ts so the policy is testable without an Electron app:

  1. not packaged — a shipped build never opens the port, and this is
     checked first so no env combination can talk it into doing so;
  2. HERMES_DESKTOP_DEV_SERVER present — an unpackaged `electron .`
     against dist/ is how the packaged app gets smoke tested, so it
     behaves like the packaged app here;
  3. the port explicitly requested and a valid integer.

Default `npm run dev` is unchanged and silent: no port, no nag. An
opt-in that gets refused always logs why, so nobody loses an hour
wondering what isn't listening.

The address is pinned to 127.0.0.1 rather than left to Chromium's
default, and is deliberately not configurable — there's no reason to
expose a renderer debugger off-host and offering the knob invites
someone to try.

scripts/eval.mjs hardcoded :9222 and threw a raw ECONNREFUSED stack when
nothing was there. It now honours the same variable and explains itself.
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on d76d083

ℹ️ Info

Desktop E2E visual evidence · View test artifacts · View job

1 visual diff.

inline evidence upload failed.

Failed to upload diff-665a0833239e-onboarding-overlay-diff.png with gh image (exit code 1): Error uploading /home/runner/work/_temp/e2e-evidence/diff-665a0833239e-onboarding-overlay-diff.png: step 0 (get upload token): uploadToken not found on repo page — do you have write access to NousResearch/hermes-agent? (or, if NousResearch enforces SAML SSO, authorize at https://github.com/orgs/NousResearch/sso)

@alt-glitch alt-glitch added type/feature New feature or request comp/desktop Electron desktop app (apps/desktop/*) javascript Pull requests that update javascript code P3 Low — cosmetic, nice to have labels Jul 28, 2026
Gating this behind an opt-in was the wrong call. A dev server already
executes arbitrary local JS — vite's module graph, every postinstall in
node_modules — so a loopback debugging port does not meaningfully widen
what a `npm run dev` session can already do, and `perf:serve` has opened
one unconditionally all along.

Requiring the variable also defeated the point: the tooling exists to be
reached for mid-task, and a capability you must remember to enable before
launching is one you don't have when you need it.

So the port opens on 9222 — the same port scripts/eval.mjs and
scripts/perf/lib/cdp.mjs already default to — for any dev-server run.
HERMES_DESKTOP_CDP_PORT stops being an on-switch and becomes an
override: a different port, or `off` to disable.

The hard gate is unchanged and still checked first: a packaged build
never opens the port, and no env value talks it into it. Neither does an
unpackaged `electron .` against dist/, which is how the packaged app
gets smoke tested.

Refusals only log when they contradict something the developer asked for
(a typo'd port, an explicit `off`). Packaged and dist runs are closed by
design and stay quiet.
@OutThisLife OutThisLife changed the title feat(desktop): opt-in renderer debugging port for dev runs feat(desktop): renderer debugging port for dev runs Jul 28, 2026
@OutThisLife
OutThisLife enabled auto-merge July 28, 2026 04:56
@OutThisLife
OutThisLife disabled auto-merge July 28, 2026 04:58
The port is only half of it. Ships the skill that tells the agent the
capability exists, when reaching for it beats reading .tsx, and how not
to hurt the user's running app while using it.

Lands in skills/software-development/ next to node-inspect-debugger,
which covers the same protocol for Node/perf work — this one is the
DOM/CSS half.

Load-bearing parts: don't relaunch or kill the user's app to get a port
(a mid-serve kill nukes Chromium's socket pool and the fallout gets
blamed on the last CSS edit); never dump the whole DOM into context;
prefer the maintained SELECTORS map to invented querySelectors; and
CDP answers factual questions only — whether it *looks* right is still
the user's call.
@OutThisLife OutThisLife changed the title feat(desktop): renderer debugging port for dev runs feat(desktop): let the agent inspect the desktop app it's developing Jul 28, 2026
@OutThisLife
OutThisLife merged commit 6384121 into main Jul 28, 2026
46 checks passed
@OutThisLife
OutThisLife deleted the bb/desktop-dev-cdp-port branch July 28, 2026 08:03
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…v-cdp-port

feat(desktop): let the agent inspect the desktop app it's developing
swissly added a commit to swissly/hermes-agent that referenced this pull request Aug 12, 2026
The skill was added in NousResearch#73121 without a risk field. 38/39 bundled
skills carry risk; this one is a read-only inspection skill (CDP DOM
reads), so classify it READ to satisfy frontmatter validation.
33hodl pushed a commit to 33hodl/hermes-agent that referenced this pull request Aug 12, 2026
…v-cdp-port

feat(desktop): let the agent inspect the desktop app it's developing
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron desktop app (apps/desktop/*) javascript Pull requests that update javascript code P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants