diff --git a/src/utils/computerUse/drainRunLoop.ts b/src/utils/computerUse/drainRunLoop.ts index 9788766e9..f9d5c3c62 100644 --- a/src/utils/computerUse/drainRunLoop.ts +++ b/src/utils/computerUse/drainRunLoop.ts @@ -18,7 +18,7 @@ let pump: ReturnType | undefined let pending = 0 function drainTick(cu: ReturnType): void { - ;(cu as any)?._drainMainRunLoop?.() + cu._drainMainRunLoop?.() } function retain(): void { diff --git a/src/utils/computerUse/escHotkey.ts b/src/utils/computerUse/escHotkey.ts index f58bdff0f..428551ed6 100644 --- a/src/utils/computerUse/escHotkey.ts +++ b/src/utils/computerUse/escHotkey.ts @@ -26,7 +26,7 @@ export function registerEscHotkey(onEscape: () => void): boolean { if (process.platform !== 'darwin') return false if (registered) return true const cu = requireComputerUseSwift() - if (!(cu as any).hotkey?.registerEscape(onEscape)) { + 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' }) @@ -41,7 +41,7 @@ export function registerEscHotkey(onEscape: () => void): boolean { export function unregisterEscHotkey(): void { if (!registered) return try { - (requireComputerUseSwift() as any).hotkey?.unregister() + requireComputerUseSwift().hotkey?.unregister() } finally { releasePump() registered = false @@ -51,5 +51,5 @@ export function unregisterEscHotkey(): void { export function notifyExpectedEscape(): void { if (!registered) return - (requireComputerUseSwift() as any).hotkey?.notifyExpectedEscape() + requireComputerUseSwift().hotkey?.notifyExpectedEscape() } diff --git a/src/utils/computerUse/executorCrossPlatform.ts b/src/utils/computerUse/executorCrossPlatform.ts index e911d1afd..27f6463a2 100644 --- a/src/utils/computerUse/executorCrossPlatform.ts +++ b/src/utils/computerUse/executorCrossPlatform.ts @@ -40,6 +40,7 @@ import { CLI_CU_CAPABILITIES, CLI_HOST_BUNDLE_ID } from './common.js' import { validateHwnd } from './win32/shared.js' import { loadPlatform } from './platforms/index.js' import type { Platform } from './platforms/index.js' +import type { WindowAction } from './platforms/types.js' // --------------------------------------------------------------------------- // Helpers for HWND-bound mode @@ -62,6 +63,19 @@ function isBound(): boolean { return getBoundHwndStr() !== null } +function isWindowAction(action: string): action is WindowAction { + return ( + action === 'minimize' || + action === 'maximize' || + action === 'restore' || + action === 'close' || + action === 'focus' || + action === 'move_offscreen' || + action === 'move_resize' || + action === 'get_rect' + ) +} + /** * Get the non-client area offset (title bar height, left border width). * Returns {dx, dy} to subtract from window coords → client coords. @@ -207,7 +221,7 @@ export function createCrossPlatformExecutor(opts: { `[computer-use] cross-platform executor for ${process.platform}`, ) - return { + const executor: ComputerExecutor = { capabilities: { ...CLI_CU_CAPABILITIES, hostBundleId: CLI_HOST_BUNDLE_ID, @@ -491,12 +505,12 @@ $i = New-Object MUp+INPUT; $i.type=0; $i.mi.dwFlags=0x0004; [MUp]::SendInput(1, await sleep(16) } // mouseDown - await (this as any).mouseDown() + await executor.mouseDown() await sleep(50) await platform.input.moveMouse(to.x, to.y) await sleep(16) // mouseUp - await (this as any).mouseUp() + await executor.mouseUp() }, async scroll(x: number, y: number, dx: number, dy: number): Promise { @@ -553,7 +567,8 @@ $i = New-Object MUp+INPUT; $i.type=0; $i.mi.dwFlags=0x0004; [MUp]::SendInput(1, // ── Window management (Windows only) ────────────────────────────── async manageWindow(action: string, opts?): Promise { if (!platform.windowManagement) return false - const result = platform.windowManagement.manageWindow(action as any, opts) + if (!isWindowAction(action)) return false + const result = platform.windowManagement.manageWindow(action, opts) // Invalidate NC offset cache on window state change _ncOffset = null _ncOffsetHwnd = null @@ -1138,6 +1153,7 @@ $i = New-Object MUp+INPUT; $i.type=0; $i.mi.dwFlags=0x0004; [MUp]::SendInput(1, } }, } + return executor } /** diff --git a/src/utils/computerUse/hostAdapter.ts b/src/utils/computerUse/hostAdapter.ts index 4a21b71e8..6e8474b05 100644 --- a/src/utils/computerUse/hostAdapter.ts +++ b/src/utils/computerUse/hostAdapter.ts @@ -36,7 +36,11 @@ class DebugLogger implements Logger { function checkAccessibilityJXA(): boolean { try { const result = Bun.spawnSync({ - cmd: ['osascript', '-e', 'tell application "System Events" to get name of every process whose background only is false'], + cmd: [ + 'osascript', + '-e', + 'tell application "System Events" to get name of every process whose background only is false', + ], stdout: 'pipe', stderr: 'pipe', }) @@ -80,7 +84,7 @@ export function getComputerUseHostAdapter(): ComputerUseHostAdapter { ensureOsPermissions: async () => { if (process.platform !== 'darwin') return { granted: true } const cu = requireComputerUseSwift() - const tcc = (cu as any).tcc + const tcc = cu.tcc // Native Swift .node module provides tcc.checkAccessibility/checkScreenRecording. // When absent (decompiled/reverse-engineered build), fall back to JXA probes. if (tcc) { diff --git a/src/utils/computerUse/swiftLoader.ts b/src/utils/computerUse/swiftLoader.ts index 0d8d7908a..34dc2d05e 100644 --- a/src/utils/computerUse/swiftLoader.ts +++ b/src/utils/computerUse/swiftLoader.ts @@ -1,19 +1,32 @@ import type { ComputerUseAPI } from '@ant/computer-use-swift' -let cached: ComputerUseAPI | undefined +export type ComputerUseSwiftAPI = ComputerUseAPI & { + hotkey?: { + registerEscape(onEscape: () => void): boolean + unregister(): void + notifyExpectedEscape(): void + } + tcc?: { + checkAccessibility(): boolean + checkScreenRecording(): boolean + } + _drainMainRunLoop?: () => void +} + +let cached: ComputerUseSwiftAPI | undefined /** * macOS-only loader for @ant/computer-use-swift. * Non-darwin platforms should use src/utils/computerUse/platforms/ instead. */ -export function requireComputerUseSwift(): ComputerUseAPI { +export function requireComputerUseSwift(): ComputerUseSwiftAPI { if (cached) return cached // eslint-disable-next-line @typescript-eslint/no-require-imports const mod = require('@ant/computer-use-swift') if (mod.ComputerUseAPI && typeof mod.ComputerUseAPI === 'function') { - cached = new mod.ComputerUseAPI() as ComputerUseAPI + cached = new mod.ComputerUseAPI() as ComputerUseSwiftAPI } else { - cached = mod as ComputerUseAPI + cached = mod as ComputerUseSwiftAPI } return cached } diff --git a/src/utils/computerUse/win32/bridgeClient.ts b/src/utils/computerUse/win32/bridgeClient.ts index 3e97e8b9f..a7461199d 100644 --- a/src/utils/computerUse/win32/bridgeClient.ts +++ b/src/utils/computerUse/win32/bridgeClient.ts @@ -23,6 +23,10 @@ interface BridgeResponse { error?: string } +type FlushableWritable = Writable & { + flush?: () => void +} + let bridgeProc: ReturnType | null = null let requestId = 0 const pendingRequests = new Map< @@ -131,10 +135,10 @@ export async function call( try { const stdin = bridgeProc!.stdin if (stdin) { - const writable = stdin as unknown as Writable + const writable = stdin as unknown as FlushableWritable writable.write(JSON.stringify(req) + '\n') - if (typeof (writable as any).flush === 'function') { - (writable as any).flush() + if (typeof writable.flush === 'function') { + writable.flush() } } } catch (err) { diff --git a/src/utils/filePersistence/outputsScanner.ts b/src/utils/filePersistence/outputsScanner.ts index f1ce6516f..71d8f22de 100644 --- a/src/utils/filePersistence/outputsScanner.ts +++ b/src/utils/filePersistence/outputsScanner.ts @@ -8,11 +8,17 @@ */ import * as fs from 'fs/promises' +import type { Dirent } from 'fs' import * as path from 'path' import { logForDebugging } from '../debug.js' import type { EnvironmentKind } from '../teleport/environments.js' import type { TurnStartTime } from './types.js' +type RecursiveDirent = Dirent & { + parentPath?: string + path?: string +} + /** Shared debug logger for file persistence modules */ export function logDebug(message: string): void { logForDebugging(`[file-persistence] ${message}`) @@ -63,13 +69,14 @@ export async function findModifiedFiles( turnStartTime: TurnStartTime, outputsDir: string, ): Promise { + const turnStartMs = turnStartTime.turnStartTime // Use recursive flag to get all entries in one call - let entries: Awaited> | any[] + let entries: RecursiveDirent[] try { entries = await fs.readdir(outputsDir, { withFileTypes: true, recursive: true, - }) as any[] + }) } catch { // Directory doesn't exist or is not accessible return [] @@ -113,7 +120,7 @@ export async function findModifiedFiles( // Filter to files modified since turn start const modifiedFiles: string[] = [] for (const result of statResults) { - if (result && result.mtimeMs >= (turnStartTime as any as number)) { + if (result && result.mtimeMs >= turnStartMs) { modifiedFiles.push(result.filePath) } } diff --git a/src/utils/log.ts b/src/utils/log.ts index 7b85e4cf7..a8bdc75e7 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -1,5 +1,6 @@ import { feature } from 'bun:bundle' import type { BetaMessageStreamParams } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' +import type { Dirent } from 'fs' import { readdir, readFile, stat } from 'fs/promises' import memoize from 'lodash-es/memoize.js' import { join } from 'path' @@ -228,9 +229,9 @@ export async function getErrorLogByIndex( * @private */ async function loadLogList(path: string): Promise { - let files: Awaited> + let files: Dirent[] try { - files = (await readdir(path, { withFileTypes: true })) as any + files = await readdir(path, { withFileTypes: true }) } catch { logError(new Error(`No logs found at ${path}`)) return [] diff --git a/src/utils/performanceShim.ts b/src/utils/performanceShim.ts index 2044d74ee..34a69f44d 100644 --- a/src/utils/performanceShim.ts +++ b/src/utils/performanceShim.ts @@ -25,6 +25,14 @@ const measures = new Map< { name: string; startTime: number; duration: number } >() +declare global { + var __performanceShimInstalled: boolean | undefined +} + +type PerformanceWithUndiciResourceTiming = Performance & { + markResourceTiming(...args: unknown[]): void +} + function now(): number { return original.now() } @@ -124,7 +132,10 @@ function clearMeasures(name?: string): void { // Plain object shim — must NOT inherit from Performance.prototype because // native getters (onresourcetimingbufferfull, timeOrigin, toJSON) check // that `this` is an actual JSC Performance instance and throw otherwise. -const shim = { +const originalWithResourceTiming = + original as PerformanceWithUndiciResourceTiming + +const shim: PerformanceWithUndiciResourceTiming = { now, mark, measure: measure as typeof performance.measure, @@ -137,21 +148,21 @@ const shim = { (() => {}) as typeof performance.setResourceTimingBufferSize, // Node.js v22 undici internal calls this after every fetch — must exist to // avoid TypeError: markResourceTiming is not a function - markResourceTiming: (() => {}) as any, + markResourceTiming: () => {}, // Delegate read-only properties to the original get timeOrigin() { return original.timeOrigin }, get onresourcetimingbufferfull() { - return (original as any).onresourcetimingbufferfull + return originalWithResourceTiming.onresourcetimingbufferfull }, - set onresourcetimingbufferfull(_v: any) { + set onresourcetimingbufferfull(_v: Performance['onresourcetimingbufferfull']) { // no-op — prevent accumulation }, toJSON() { return original.toJSON() }, -} as unknown as typeof performance +} as unknown as PerformanceWithUndiciResourceTiming /** * Install the shim onto globalThis.performance. Safe to call multiple times. @@ -159,8 +170,8 @@ const shim = { * native Performance reference. */ export function installPerformanceShim(): void { - if ((globalThis as any).__performanceShimInstalled) return - ;(globalThis as any).__performanceShimInstalled = true + if (globalThis.__performanceShimInstalled) return + globalThis.__performanceShimInstalled = true globalThis.performance = shim } diff --git a/src/utils/settings/validateEditTool.ts b/src/utils/settings/validateEditTool.ts index 1d245911f..beb939371 100644 --- a/src/utils/settings/validateEditTool.ts +++ b/src/utils/settings/validateEditTool.ts @@ -33,10 +33,10 @@ export function validateInputForSettingsFileEdit( const updatedContent = getUpdatedContent() const afterValidation = validateSettingsFileContent(updatedContent) - if (!afterValidation.isValid) { + if (afterValidation.isValid === false) { return { result: false, - message: `Claude Code settings.json validation failed after edit:\n${(afterValidation as any).error}\n\nFull schema:\n${(afterValidation as any).fullSchema}\nIMPORTANT: Do not update the env unless explicitly instructed to do so.`, + message: `Claude Code settings.json validation failed after edit:\n${afterValidation.error}\n\nFull schema:\n${afterValidation.fullSchema}\nIMPORTANT: Do not update the env unless explicitly instructed to do so.`, errorCode: 10, } } diff --git a/src/utils/transcriptSearch.ts b/src/utils/transcriptSearch.ts index e8219c4a2..57ffa29be 100644 --- a/src/utils/transcriptSearch.ts +++ b/src/utils/transcriptSearch.ts @@ -16,6 +16,19 @@ const RENDERED_AS_SENTINEL = new Set([ const searchTextCache = new WeakMap() +function isTextContentBlock( + block: unknown, +): block is { type: 'text'; text: string } { + return ( + typeof block === 'object' && + block !== null && + 'type' in block && + block.type === 'text' && + 'text' in block && + typeof block.text === 'string' + ) +} + /** Flatten a RenderableMessage to lowercased searchable text. WeakMap- * cached — messages are append-only and immutable so a hit is always * valid. Lowercased at cache time: the only caller immediately @@ -38,7 +51,7 @@ function computeSearchText(msg: RenderableMessage): string { raw = RENDERED_AS_SENTINEL.has(c) ? '' : c } else { const parts: string[] = [] - for (const b of (c ?? [])) { + for (const b of c ?? []) { if (b.type === 'text') { if (!RENDERED_AS_SENTINEL.has(b.text)) parts.push(b.text) } else if (b.type === 'tool_result') { @@ -84,7 +97,9 @@ function computeSearchText(msg: RenderableMessage): string { // (AttachmentMessage.tsx {m.content}). Visible but // unsearchable without this — [ dump finds it, / doesn't. if (msg.attachment!.type === 'relevant_memories') { - raw = (msg.attachment!.memories ?? []).map((m: { content: string }) => m.content).join('\n') + raw = (msg.attachment!.memories ?? []) + .map((m: { content: string }) => m.content) + .join('\n') } else if ( // Mid-turn prompts — queued while an agent is running. Render via // UserTextMessage (AttachmentMessage.tsx:~348). stickyPromptText @@ -97,7 +112,11 @@ function computeSearchText(msg: RenderableMessage): string { raw = typeof p === 'string' ? p - : (p as any[]).flatMap(b => (b.type === 'text' ? [b.text] : [])).join('\n') + : Array.isArray(p) + ? p + .flatMap(b => (isTextContentBlock(b) ? [b.text] : [])) + .join('\n') + : '' } break }