import type { ToolUseContext } from '../../Tool.js' import { logForDebugging } from '../debug.js' import { errorMessage } from '../errors.js' import { withResolvers } from '../withResolvers.js' import { isLockHeldLocally, releaseComputerUseLock } from './computerUseLock.js' import { unregisterEscHotkey } from './escHotkey.js' // cu.apps.unhide is NOT one of the four @MainActor methods wrapped by // drainRunLoop's 30s backstop. On abort paths (where the user hit Ctrl+C // because something was slow) a hang here would wedge the abort. Generous // timeout — unhide should be ~instant; if it takes 5s something is wrong // and proceeding is better than waiting. The Swift call continues in the // background regardless; we just stop blocking on it. const UNHIDE_TIMEOUT_MS = 5000 /** * Turn-end cleanup for the chicago MCP surface: auto-unhide apps that * `prepareForAction` hid, then release the file-based lock. * * Called from three sites: natural turn end (`stopHooks.ts`), abort during * streaming (`query.ts` aborted_streaming), abort during tool execution * (`query.ts` aborted_tools). All three reach this via dynamic import gated * on `feature('CHICAGO_MCP')`. `executor.js` (which pulls both native * modules) is dynamic-imported below so non-CU turns don't load native * modules just to no-op. * * No-ops cheaply on non-CU turns: both gate checks are zero-syscall. */ export async function cleanupComputerUseAfterTurn( ctx: Pick< ToolUseContext, 'getAppState' | 'setAppState' | 'sendOSNotification' >, ): Promise { const appState = ctx.getAppState() const hidden = appState.computerUseMcpState?.hiddenDuringTurn if (hidden && hidden.size > 0) { const { unhideComputerUseApps } = await import('./executor.js') const unhide = unhideComputerUseApps([...hidden]).catch(err => logForDebugging( `[Computer Use MCP] auto-unhide failed: ${errorMessage(err)}`, ), ) const timeout = withResolvers() const timer = setTimeout(timeout.resolve, UNHIDE_TIMEOUT_MS) await Promise.race([unhide, timeout.promise]).finally(() => clearTimeout(timer), ) ctx.setAppState(prev => prev.computerUseMcpState?.hiddenDuringTurn === undefined ? prev : { ...prev, computerUseMcpState: { ...prev.computerUseMcpState, hiddenDuringTurn: undefined, }, }, ) } // Zero-syscall pre-check so non-CU turns don't touch disk. Release is still // idempotent (returns false if already released or owned by another session). if (!isLockHeldLocally()) return // Unregister before lock release so the pump-retain drops as soon as the // CU session ends. Idempotent — no-ops if registration failed at acquire. // Swallow throws so a NAPI unregister error never prevents lock release — // a held lock blocks the next CU session with "in use by another session". try { unregisterEscHotkey() } catch (err) { logForDebugging( `[Computer Use MCP] unregisterEscHotkey failed: ${errorMessage(err)}`, ) } if (await releaseComputerUseLock()) { ctx.sendOSNotification?.({ message: 'Claude is done using your computer', notificationType: 'computer_use_exit', }) } }