Files
hermes-agent/apps/desktop/scripts/click-session-helpers.mjs
Brooklyn Nicholson 959c7649fd fix(updates/win): click-session poll, Intel-Mac installer docs, cua-driver opt-in autostart
click-session flake (#97982): a bare scrollIntoView() smooth scroll could be dropped under load, and the script
slept a fixed 3000ms before reading state. Extract click-session-helpers.mjs:
instant centered scroll, a bounded poll-for-composer loop instead of the
fixed sleep, and correct nested CDP envelope unwrapping (the old read logged
undefined).

Intel-Mac installer docs (#99033): the Hermes-Setup.dmg bootstrap installer
is built for Apple Silicon only, so Intel Macs hit "not supported on this
Mac". The desktop release pipeline already builds a native darwin-x64
bundle, so the docs now scope the arm64 limit to the bootstrap installer and
name the darwin-x64 bundle (or the CLI plus `hermes desktop`) as the Intel
path, in the desktop README and the platform-support build-targets section.

cua-driver autostart opt-in (#97389): Windows installs registered the
cua-driver-serve scheduled task on every install, with no opt-out, and
treated the task as an install-readiness requirement. Gate the
install-ready check and _repair_cua_driver_autostart_windows on the new
computer_use.autostart config key (default false = on-demand, fails
closed), extract the registration PowerShell into a testable helper, and
document the opt-in (EN + zh-Hans). Windows-only code path: unit tests
cover the registration args and the config gate; live Windows
verification pending.
2026-09-26 17:09:16 -05:00

89 lines
3.6 KiB
JavaScript

/**
* click-session-helpers.mjs
*
* Shared logic for scripts/click-session.mjs — the interactive CDP helper that
* clicks a sidebar session by partial title and reads the post-click state.
*
* The macOS check-suite flake (#97982): the old script did a bare
* `scrollIntoView()` (smooth-scroll default on some surfaces; scroll can be
* dropped entirely under load) followed by a fixed `setTimeout(3000)` before
* reading state — a dropped scroll silently missed the click, and the fixed
* sleep both raced fast loads and wasted time on slow ones. The helpers here
* make the two steps deterministic:
*
* - buildFindClickExpression: instant scroll (`behavior: 'auto'`,
* `block: 'center'`) so the element is where we clicked it.
* - pollUntil: bounded visibility/readiness polling instead of a fixed
* sleep — it returns as soon as the predicate expression is true.
* - evaluateJsonValue: unwraps the nested CDP envelope
* ({ result: { result: { value } } }) the old script read one level short,
* so the post-click state log printed `undefined`.
*
* Dependency-free (plain node) so it runs under the vitest electron project.
*/
export const CLICK_POLL_INTERVAL_MS = 100
export const CLICK_POLL_TIMEOUT_MS = 3000
/**
* Runtime.evaluate expression: find the first button-ish element whose text
* contains `titleMatch`, instantly scroll it into view (center of the
* viewport, no smooth animation to drop), and click it. Returns JSON:
* { found, tried? } when there is no match, { found: true, tag, text }
* when the click was issued.
*/
export function buildFindClickExpression(titleMatch) {
return `
(() => {
const titleMatch = ${JSON.stringify(titleMatch)}
const all = document.querySelectorAll('button, a, div[role="button"]')
const found = [...all].find(el => (el.textContent || '').includes(titleMatch))
if (!found) return JSON.stringify({ found: false, tried: titleMatch })
found.scrollIntoView({ behavior: 'auto', block: 'center' })
found.click()
return JSON.stringify({ found: true, tag: found.tagName, text: (found.textContent || '').slice(0, 80) })
})()
`
}
/** Runtime.evaluate expression answering `true` once the session view is live
* (the composer for the clicked session has mounted). */
export const POST_CLICK_READY_EXPRESSION =
'JSON.stringify(!!document.querySelector(\'[data-slot="composer-rich-input"]\'))'
/**
* Unwrap the nested CDP envelope. `send()` resolves with the whole WebSocket
* message: `{ id, result: { result: { type, value } } }`. Returns the parsed
* JSON of `message.result.result.value`, or null when any level is missing.
*/
export function evaluateJsonValue(message) {
const raw = message?.result?.result?.value ?? message?.result?.value ?? null
if (raw == null) return null
try {
return JSON.parse(raw)
} catch {
return null
}
}
/**
* Poll `predicate(send)` (a Runtime.evaluate expression that JSON-encodes a
* boolean) until it answers true or `timeoutMs` elapses. Resolves true/false —
* never throws on timeout — so a slow session surfaces as `ready: false`
* state instead of a hung script.
*/
export async function pollUntil(send, expression, {
timeoutMs = CLICK_POLL_TIMEOUT_MS,
intervalMs = CLICK_POLL_INTERVAL_MS
} = {}) {
const deadline = Date.now() + timeoutMs
for (;;) {
const message = await send('Runtime.evaluate', { expression, returnByValue: true })
const value = evaluateJsonValue(message)
if (value === true) return true
if (Date.now() + intervalMs >= deadline) return false
await new Promise(resolve => setTimeout(resolve, intervalMs))
}
}