import { logForDebugging } from '../debug.js' import { releasePump, retainPump } from './drainRunLoop.js' import { requireComputerUseSwift } from './swiftLoader.js' /** * Global Escape → abort. Mirrors Cowork's `escAbort.ts` but without Electron: * CGEventTap via `@ant/computer-use-swift`. While registered, Escape is * consumed system-wide (PI defense — a prompt-injected action can't dismiss * a dialog with Escape). * * Lifecycle: register on fresh lock acquire (`wrapper.tsx` `acquireCuLock`), * unregister on lock release (`cleanup.ts`). The tap's CFRunLoopSource sits * in .defaultMode on CFRunLoopGetMain(), so we hold a drainRunLoop pump * retain for the registration's lifetime — same refcounted setInterval as * the `@MainActor` methods. * * `notifyExpectedEscape()` punches a hole for model-synthesized Escapes: the * executor's `key("escape")` calls it before posting the CGEvent. Swift * schedules a 100ms decay so a CGEvent that never reaches the tap callback * doesn't eat the next user ESC. */ let registered = false export function registerEscHotkey(onEscape: () => void): boolean { if (registered) return true const cu = requireComputerUseSwift() if (!cu.hotkey.registerEscape(onEscape)) { // CGEvent.tapCreate failed — typically missing Accessibility permission. // CU still works, just without ESC abort. Mirrors Cowork's escAbort.ts:81. logForDebugging('[cu-esc] registerEscape returned false', { level: 'warn' }) return false } retainPump() registered = true logForDebugging('[cu-esc] registered') return true } export function unregisterEscHotkey(): void { if (!registered) return try { requireComputerUseSwift().hotkey.unregister() } finally { releasePump() registered = false logForDebugging('[cu-esc] unregistered') } } export function notifyExpectedEscape(): void { if (!registered) return requireComputerUseSwift().hotkey.notifyExpectedEscape() }