--- name: enso-cdp description: > Drive the EnsoCode Electron renderer via Chrome DevTools Protocol on port 9222. Use when verifying UI in the running app, sending a composer chat turn, reading conversation/timeline state, screenshots, CDP, 真机验证, remote-debugging-port, or after changing renderer/main/agent/preload. Use when the user runs /enso-cdp. --- # EnsoCode CDP Dev (`!app.isPackaged`) already opens **9222** in `src/main/index.ts`. Do not inject `TEMP-DEBUG`. Packaged builds stay closed. ## Scripts first 免依赖脚本在 `scripts/`(相对本 skill 目录),优先用它们,不要重写 connect 样板: ```bash node scripts/cdp.mjs pages # 列出 page 目标 node scripts/cdp.mjs eval '(async()=>{...})()' # 主窗口执行 JS(支持 await) node scripts/cdp.mjs eval '...' --page settings.html # 设置窗口 node scripts/cdp.mjs shot /tmp/x.png # 截图 node scripts/cdp.mjs keys ArrowDown Enter # 受信任按键(真实输入管线) node scripts/cdp.mjs type '@rev' # 受信任文本输入 node scripts/cdp.mjs drag '' '' # 受信任鼠标拖拽(from/to 为返回 {x,y} 的 JS) ``` 合成事件(eval 里 dispatchEvent)`isTrusted=false` 且不过 IME/焦点管线;排查“真实键盘行为”用 `keys`/`type`。 `keys Enter` 只发 `rawKeyDown`/`keyUp`,不产生字符事件,**不会触发 `
` 的隐式提交**;验表单回车要手发 `Input.dispatchKeyEvent {type:'keyDown', text:'\r', key:'Enter', windowsVirtualKeyCode:13}`,或直接点提交按钮。 ## 鼠标拖拽 / 点击(Input.dispatchMouseEvent) **窗口必须可见**:`document.visibilityState === 'hidden'`(窗口被遮挡/后台/另一 Space)时, Chromium 会静默丢弃 CDP 的 `mousePressed`/`mouseReleased`(`mouseMoved` 照常送达)—— 表现为拖拽/点击“时好时坏”,极易误判为产品 bug。`Page.bringToFront` 无效,要 OS 级激活: ```bash osascript -e 'tell application "System Events" to set frontmost of (first process whose name contains "Electron") to true' ``` `cdp.mjs drag` 已内置这个检查与自动激活;手写鼠标序列时先 eval `document.visibilityState` 确认。 其它要点: - dnd-kit 等 PointerSensor 库必须走真实指针管线;eval 里合成 PointerEvent/HTML5 drag 事件无效。 - 拖拽需要 press 后**分段** move(越过 activation 距离阈值),落点前悬停 ~150ms 让 droppable 识别。 - 拖到“拖动中才出现的落点”(如临时 drop bar):先 press+小幅 move 激活拖拽,再 query 落点坐标继续 move。 - 验证断言别用 class 子串(如 `border-ring`):Tailwind 的 `focus-within:border-ring` 会让 `className.includes()` 永真。 隔离验证环境(不碰真实 userData): ```bash node scripts/mk-env.mjs /tmp/enso-x 2 # 2 个 fake provider(凭证尾号可区分) node /scripts/fake-provider-issue-27.mjs & # 8899;支持 [[tool:name {json}]] 指令 ENSO_USER_DATA_DIR=/tmp/enso-x ENSO_CDP_PORT=9333 pnpm dev ENSO_CDP_PORT=9333 node scripts/cdp.mjs pages curl -s http://127.0.0.1:8899/__requests # 每个请求实际带的凭证尾号 ``` **隔离验证必须错开 CDP 端口**:用户自己的 dev 实例(真实 userData)常驻 9222,第二个实例绑不上端口也不报错,`cdp.mjs` 会静默连到用户实例,写操作直接落进真实数据。动手前先 `lsof -nP -iTCP: -sTCP:LISTEN` 确认 PID 属于隔离实例(`ps -E -p ` 能看到 `ENSO_USER_DATA_DIR`)。 常用 store 入口(eval 里):`window.__stores.sessions.getState()` / `window.__stores.settings.getState()`;`window.electronAPI.*` 是 preload API。 ## Connect(手写时才看) 1. `pnpm dev` is running. 2. `curl -s http://127.0.0.1:9222/json/version` — if this fails, restart dev (port is not HMR). 3. Pick **`type === "page"`**, not `json/list[0]` (a `browser` target may come first). Main window is `http://localhost:5173/index.html`. ```bash curl -s http://127.0.0.1:9222/json/list \ | python3 -c "import json,sys; d=json.load(sys.stdin); p=next(x for x in d if x.get('type')=='page'); print(p['webSocketDebuggerUrl'])" ``` Node ≥ 22 has global `WebSocket`. Enable CDP, then evaluate with monotonic ids: ```js await send('Runtime.enable'); async function evalExpr(expression) { const res = await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true }); if (res.result?.exceptionDetails) throw new Error(JSON.stringify(res.result.exceptionDetails)); return res.result?.result?.value; } ``` `send` must map replies by `id`. Renderer vite root is `src/renderer`: dynamic import `/stores/...`, never `/src/renderer/stores/...`. ## Read - Live chat: `document.body.innerText` (tail `slice(-1200)` for the latest turn). Messages are **not** in localStorage. - Metadata only: `localStorage['enso-conversations']` — `messages` is always `[]`. - Composer: `document.querySelector('textarea')`. - Diff widgets: read `el.shadowRoot`, not light DOM. - Viewport: chat scroller is `[data-slot=scroll-area-viewport]`, not `window`. - Screenshot: `Page.captureScreenshot` `{ format: 'png' }`. ## Send (composer) Controlled React textarea: assigning `.value` does not update state. Native setter + `input` + Enter (no Shift): ```js await evalExpr(`(() => { const ta = document.querySelector('textarea'); const proto = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value'); proto.set.call(ta, 'YOUR_PROMPT'); ta.dispatchEvent(new Event('input', { bubbles: true })); ta.focus(); ta.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', which: 13, keyCode: 13, bubbles: true, })); return ta.value; })()`); ``` Wait **5–25s** for the turn, then read the text tail. Do not assert immediately. Failed assistant text stays in the transcript. Judge “no error” on **new timestamps** or the tail, not the whole `innerText`. ## After code changes - Renderer: HMR. - `src/main` / `src/agent` / `src/preload`: restart `pnpm dev` or CDP is still the old worker. Do not leave test sessions, scratch scripts, or mutated `settings.json` / `userData` behind. Backup real user files before touching them; restore and checksum after.