/ utils / computerUse / escHotkey.ts
escHotkey.ts
 1  import { logForDebugging } from '../debug.js'
 2  import { releasePump, retainPump } from './drainRunLoop.js'
 3  import { requireComputerUseSwift } from './swiftLoader.js'
 4  
 5  /**
 6   * Global Escape → abort. Mirrors Cowork's `escAbort.ts` but without Electron:
 7   * CGEventTap via `@ant/computer-use-swift`. While registered, Escape is
 8   * consumed system-wide (PI defense — a prompt-injected action can't dismiss
 9   * a dialog with Escape).
10   *
11   * Lifecycle: register on fresh lock acquire (`wrapper.tsx` `acquireCuLock`),
12   * unregister on lock release (`cleanup.ts`). The tap's CFRunLoopSource sits
13   * in .defaultMode on CFRunLoopGetMain(), so we hold a drainRunLoop pump
14   * retain for the registration's lifetime — same refcounted setInterval as
15   * the `@MainActor` methods.
16   *
17   * `notifyExpectedEscape()` punches a hole for model-synthesized Escapes: the
18   * executor's `key("escape")` calls it before posting the CGEvent. Swift
19   * schedules a 100ms decay so a CGEvent that never reaches the tap callback
20   * doesn't eat the next user ESC.
21   */
22  
23  let registered = false
24  
25  export function registerEscHotkey(onEscape: () => void): boolean {
26    if (registered) return true
27    const cu = requireComputerUseSwift()
28    if (!cu.hotkey.registerEscape(onEscape)) {
29      // CGEvent.tapCreate failed — typically missing Accessibility permission.
30      // CU still works, just without ESC abort. Mirrors Cowork's escAbort.ts:81.
31      logForDebugging('[cu-esc] registerEscape returned false', { level: 'warn' })
32      return false
33    }
34    retainPump()
35    registered = true
36    logForDebugging('[cu-esc] registered')
37    return true
38  }
39  
40  export function unregisterEscHotkey(): void {
41    if (!registered) return
42    try {
43      requireComputerUseSwift().hotkey.unregister()
44    } finally {
45      releasePump()
46      registered = false
47      logForDebugging('[cu-esc] unregistered')
48    }
49  }
50  
51  export function notifyExpectedEscape(): void {
52    if (!registered) return
53    requireComputerUseSwift().hotkey.notifyExpectedEscape()
54  }