mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 17:47:40 +00:00
The five oversized src/main/browser modules and src/main/ipc/browser.ts each carried a file-level `eslint-disable max-lines` and ran 377-654 counted lines against a 300-line budget. AGENTS.md calls for splitting rather than suppressing, and config/max-lines-baseline.txt is a shrink-only ratchet, so this removes all six suppressions and prunes their entries (341 -> 335). Pure move, no behavior change. cdp-ws-proxy is decomposed into collaborating objects rather than free functions because its state is genuinely per-connection: every collaborator is a private readonly instance field built in the constructor with live closures over `this`, so per-connection state stays per-connection. Likewise the screencast pacer's isClosed/isStopping and snapshot capture's getSeq are live thunks, not values captured at wiring time, so guards inside already-armed timers still observe a later stop(). browser-guest-ui.ts is renamed to browser-guest-shortcut-forwarding.ts: after the split it exports exactly one function, setupGuestShortcutForwarding, so the old name no longer described its contents. Also restores a single `webContents.debugger` read in the screencast path. The extraction had left three reads where the original had one; the accessor is stable today, so this is not a behavior fix but it removes a latent divergence. Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green (remaining failures are pre-existing load flakes in untouched files, each green when re-run serially), no new runtime import cycles, and the IPC channel set diffed identical before/after with all 23 handlers still trust-gated.
103 lines
3.7 KiB
TypeScript
103 lines
3.7 KiB
TypeScript
import type { SnapshotEntry } from './snapshot-ax-tree-walk'
|
|
import type { CdpCommandSender } from './snapshot-engine'
|
|
|
|
// Why: finds DOM elements that are visually interactive (cursor:pointer, onclick,
|
|
// tabindex, contenteditable) but lack standard ARIA roles. These are common in
|
|
// modern SPAs where styled <div>s act as buttons. Returns them as a JS array of
|
|
// remote object references that we can resolve to backendNodeIds via CDP.
|
|
export async function findCursorInteractiveElements(
|
|
sendCommand: CdpCommandSender,
|
|
existingEntries: SnapshotEntry[]
|
|
): Promise<SnapshotEntry[]> {
|
|
const existingNodeIds = new Set(existingEntries.map((e) => e.backendDOMNodeId))
|
|
const results: SnapshotEntry[] = []
|
|
|
|
try {
|
|
// Single evaluate call that finds interactive elements and returns their info
|
|
// along with a way to reference them by index
|
|
const { result } = (await sendCommand('Runtime.evaluate', {
|
|
expression: `(() => {
|
|
const SKIP_ROLES = new Set(['button','link','textbox','checkbox','radio','tab',
|
|
'menuitem','option','switch','slider','combobox','searchbox','spinbutton','treeitem',
|
|
'menuitemcheckbox','menuitemradio']);
|
|
const SKIP_TAGS = new Set(['input','button','select','textarea','a']);
|
|
const seen = new Set();
|
|
const found = [];
|
|
const matchedElements = [];
|
|
|
|
function check(el) {
|
|
if (seen.has(el)) return;
|
|
seen.add(el);
|
|
const tag = el.tagName.toLowerCase();
|
|
if (SKIP_TAGS.has(tag)) return;
|
|
const role = el.getAttribute('role');
|
|
if (role && SKIP_ROLES.has(role)) return;
|
|
const rect = el.getBoundingClientRect();
|
|
if (rect.width === 0 || rect.height === 0) return;
|
|
const text = (el.ariaLabel || el.getAttribute('aria-label') || el.textContent || '').trim().slice(0, 80);
|
|
if (!text) return;
|
|
found.push({ text, tag });
|
|
matchedElements.push(el);
|
|
if (found.length >= 50) return;
|
|
}
|
|
|
|
document.querySelectorAll('[onclick], [tabindex]:not([tabindex="-1"]), [contenteditable="true"]').forEach(el => {
|
|
if (found.length < 50) check(el);
|
|
});
|
|
document.querySelectorAll('div, span, li, td, img, svg, label').forEach(el => {
|
|
if (found.length >= 50) return;
|
|
try {
|
|
if (window.getComputedStyle(el).cursor === 'pointer') check(el);
|
|
} catch {}
|
|
});
|
|
|
|
window.__orcaCursorInteractive = matchedElements;
|
|
return JSON.stringify(found);
|
|
})()`,
|
|
returnByValue: true
|
|
})) as { result: { value: string } }
|
|
|
|
const elements = JSON.parse(result.value) as { text: string; tag: string }[]
|
|
|
|
for (let i = 0; i < elements.length; i++) {
|
|
try {
|
|
const { result: objResult } = (await sendCommand('Runtime.evaluate', {
|
|
expression: `window.__orcaCursorInteractive[${i}]`
|
|
})) as { result: { objectId?: string } }
|
|
|
|
if (!objResult.objectId) {
|
|
continue
|
|
}
|
|
|
|
const { node } = (await sendCommand('DOM.describeNode', {
|
|
objectId: objResult.objectId
|
|
})) as { node: { backendNodeId: number } }
|
|
|
|
if (existingNodeIds.has(node.backendNodeId)) {
|
|
continue
|
|
}
|
|
|
|
results.push({
|
|
ref: '',
|
|
role: 'clickable',
|
|
name: elements[i].text,
|
|
backendDOMNodeId: node.backendNodeId,
|
|
depth: 0
|
|
})
|
|
} catch {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Clean up
|
|
await sendCommand('Runtime.evaluate', {
|
|
expression: 'delete window.__orcaCursorInteractive',
|
|
returnByValue: true
|
|
})
|
|
} catch {
|
|
// DOM query failed — not critical, just return empty
|
|
}
|
|
|
|
return results
|
|
}
|