inspecting-hermes-desktop-dom

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Inspecting the live Hermes desktop DOM

检查Hermes桌面端的实时DOM

Overview

概述

When you are developing
apps/desktop
and the user is running that same app (
hgui
/
npm run dev
), you can read the live rendered DOM of the window they are looking at — computed styles, geometry, which CSS rule actually won, console output — instead of inferring it from
.tsx
and being wrong.
Dev-server runs open a Chrome DevTools Protocol port on
127.0.0.1:9222
automatically. The renderer is a Chromium page, so everything DevTools can read, a script can read.
This does not replace looking at it. CDP answers factual questions ("what is the computed padding", "did this element render", "which selector matches"). It cannot tell you whether the result looks good. Colour balance, spacing feel, and "is this ugly" still need the user's eyes or a screenshot. Answer facts with CDP; hand aesthetics to the user.
当你开发
apps/desktop
且用户正在运行同一应用(
hgui
/
npm run dev
)时,你可以读取他们当前查看窗口的实时渲染DOM——包括计算样式、几何结构、实际生效的CSS规则、控制台输出——而不是从
.tsx
文件推断,避免出错。
开发服务器会自动在
127.0.0.1:9222
开启Chrome DevTools Protocol(CDP)端口。渲染器是一个Chromium页面,因此DevTools能读取的所有内容,脚本都能读取。
**这不能替代直接查看界面。**CDP只能回答事实性问题(“计算后的内边距是多少”、“这个元素是否渲染了”、“哪个选择器匹配生效”)。它无法判断结果是否美观。色彩平衡、间距观感以及“是否难看”这类问题仍需要用户的肉眼观察或截图。用CDP解答事实问题,将美学判断交给用户。

When to Use

使用场景

  • Verifying a UI change actually took effect in the running app
  • "Why is this element still X?" — find the winning rule before editing anything
  • Locating a stable selector for a component you're about to change
  • Checking a design token's computed value on a real node
  • Reading renderer console errors the user mentions but can't copy out
Don't use for: perf profiling or heap work (
node-inspect-debugger
,
debugging-hermes-desktop
), or anything where the real question is "does this look right".
  • 验证UI变更是否在运行中的应用中实际生效
  • 排查“为什么这个元素还是X状态?”——在编辑任何内容前找到实际生效的规则
  • 为即将修改的组件定位稳定的选择器
  • 检查设计令牌在真实节点上的计算值
  • 读取用户提到但无法复制的渲染器控制台错误
**不适用场景:**性能分析或堆内存相关工作(使用
node-inspect-debugger
debugging-hermes-desktop
),或任何核心问题为“这个看起来是否正常”的场景。

The port

端口说明

Open on
127.0.0.1:9222
for any dev-server run. Closed in exactly two cases (
apps/desktop/electron/dev-cdp.ts
):
  • packaged builds — always, 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 before doing anything else:
bash
curl -s --max-time 3 http://127.0.0.1:${HERMES_DESKTOP_CDP_PORT:-9222}/json/version
Empty → no port. Do not guess another port silently.
Never relaunch the user's app to get a port. That destroys their session and their state. Launch your own isolated instance instead (below).
任何开发服务器运行时都会在
127.0.0.1:9222
开启端口。仅在以下两种情况下端口会关闭(对应
apps/desktop/electron/dev-cdp.ts
):
  • 打包构建版本——始终关闭,且无法通过环境变量覆盖;
  • 未设置
    HERMES_DESKTOP_DEV_SERVER
    ——针对
    dist/
    运行未打包的
    electron .
    是打包应用的冒烟测试方式,因此其行为与打包版本一致。
通过
HERMES_DESKTOP_CDP_PORT
可修改端口(如
=9333
)或禁用端口(如
=off
)。
在执行任何操作前先检查端口状态:
bash
curl -s --max-time 3 http://127.0.0.1:${HERMES_DESKTOP_CDP_PORT:-9222}/json/version
返回空内容→端口未开启。请勿静默尝试其他端口。
**绝不要为了获取端口而重启用户的应用。**这会销毁用户的会话和状态。请启动独立的隔离实例(见下文)。

Reading the DOM

读取DOM

apps/desktop/scripts/eval.mjs
is the one-liner:
bash
cd apps/desktop
node scripts/eval.mjs "document.querySelectorAll('[data-slot]').length"
For multi-step work use the shared client — it has target discovery and promise-aware eval:
js
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 them over inventing a
querySelector
— they are updated as a unit when components move.
apps/desktop/scripts/eval.mjs
是一行式工具:
bash
cd apps/desktop
node scripts/eval.mjs "document.querySelectorAll('[data-slot]').length"
对于多步骤操作,使用共享客户端——它具备目标发现和支持Promise的eval功能:
js
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()
scripts/perf/lib/cdp.mjs
中的
SELECTORS
包含稳定的
data-slot
钩子(编辑器、线程视口、助手消息、对话对、侧边栏)。优先使用这些钩子而非自定义
querySelector
——当组件位置变动时,这些钩子会统一更新。

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:
js
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, and you need the ancestor rule. A plugin stylesheet (e.g.
@tailwindcss/typography
's
prose a { font-weight: 500 }
) routinely beats a utility class; override on the shared class, not at each usage.
因为样式“未生效”就修改所有调用位置是典型的无用功。先读取真实节点信息:
js
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
  })()
})
如果节点自身没有携带任何类,那么样式值是继承而来的——修改所有调用位置无法解决问题,你需要找到祖先元素的规则。插件样式表(如
@tailwindcss/typography
prose a { font-weight: 500 }
)通常会覆盖工具类;请在共享类上进行覆盖,而非逐个修改使用位置。

Your own isolated instance

独立隔离实例

When there is no port, or you must not disturb the user's window:
bash
cd apps/desktop
HERMES_HOME=/tmp/cdp-probe-home \
HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 \
HERMES_DESKTOP_CDP_PORT=9333 \
  npx electron . --user-data-dir=/tmp/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.
当端口未开启,或你不能干扰用户窗口时:
bash
cd apps/desktop
HERMES_HOME=/tmp/cdp-probe-home \
HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 \
HERMES_DESKTOP_CDP_PORT=9333 \
  npx electron . --user-data-dir=/tmp/cdp-probe-userdata
独立的
--user-data-dir
可避开Electron的单实例锁,因此不会与正在运行的
hgui
冲突;独立的
HERMES_HOME
可避免影响真实会话。同理,请选择9222以外的端口。在后台运行该实例,使用完毕后关闭。
如果你同时需要性能测试工具,
npm run perf:serve
会自动使用临时
HERMES_HOME
完成相同操作。

Pitfalls

注意事项

  • 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 — read promptly, and don't mistake a self-exited probe for a broken port. Chromium logs
    DevTools listening on ws://127.0.0.1:<port>/…
    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.
  • **绝不要为了“释放”资源而终止用户的开发服务器或应用。**中途终止会销毁Chromium的套接字池,导致的
    ERR_NETWORK_CHANGED
    错误会被归咎于你刚刚修改的内容。
  • **临时
    HERMES_HOME
    没有后端服务。**应用会针对
    hermes:api
    记录
    ECONNREFUSED
    错误,甚至可能自行退出。但渲染器仍会挂载,DOM仍可读取——请及时读取,不要将自行退出的探测实例误认为端口故障。Chromium在绑定端口时会记录
    DevTools listening on ws://127.0.0.1:<port>/…
    ,该日志行是端口已开启的证明。
  • **轮询检查,不要仅探测一次。**刚启动的应用需要一两秒时间才能响应端口请求。
  • **绝不要导出整个DOM。**桌面端会渲染数百个节点,
    outerHTML
    会淹没你的上下文。请在eval表达式内将结果精简为小型JSON对象。
  • **调用
    CDP.connect
    时传入
    match
    参数。**否则你可能会连接到宠物浮层、快速输入窗口或DevTools目标,而非主窗口。
  • cdp.eval
    直接返回值;原生
    Runtime.evaluate
    会返回双层嵌套结构
    .result.result.value
    )。请使用封装后的方法。
  • 在本仓库的
    vite dev
    环境下,
    import.meta.env.DEV
    的值为
    true
    apps/desktop/scripts/profile-typing-lag.md
    中相反的说明已过时。