fix: remove mid-plan as-any casts

This commit is contained in:
James Feng 2026-06-07 15:33:40 +08:00
parent 33a6217702
commit c130eaad4b
11 changed files with 109 additions and 34 deletions

View File

@ -18,7 +18,7 @@ let pump: ReturnType<typeof setInterval> | undefined
let pending = 0 let pending = 0
function drainTick(cu: ReturnType<typeof requireComputerUseSwift>): void { function drainTick(cu: ReturnType<typeof requireComputerUseSwift>): void {
;(cu as any)?._drainMainRunLoop?.() cu._drainMainRunLoop?.()
} }
function retain(): void { function retain(): void {

View File

@ -26,7 +26,7 @@ export function registerEscHotkey(onEscape: () => void): boolean {
if (process.platform !== 'darwin') return false if (process.platform !== 'darwin') return false
if (registered) return true if (registered) return true
const cu = requireComputerUseSwift() const cu = requireComputerUseSwift()
if (!(cu as any).hotkey?.registerEscape(onEscape)) { if (!cu.hotkey?.registerEscape(onEscape)) {
// CGEvent.tapCreate failed — typically missing Accessibility permission. // CGEvent.tapCreate failed — typically missing Accessibility permission.
// CU still works, just without ESC abort. Mirrors Cowork's escAbort.ts:81. // CU still works, just without ESC abort. Mirrors Cowork's escAbort.ts:81.
logForDebugging('[cu-esc] registerEscape returned false', { level: 'warn' }) logForDebugging('[cu-esc] registerEscape returned false', { level: 'warn' })
@ -41,7 +41,7 @@ export function registerEscHotkey(onEscape: () => void): boolean {
export function unregisterEscHotkey(): void { export function unregisterEscHotkey(): void {
if (!registered) return if (!registered) return
try { try {
(requireComputerUseSwift() as any).hotkey?.unregister() requireComputerUseSwift().hotkey?.unregister()
} finally { } finally {
releasePump() releasePump()
registered = false registered = false
@ -51,5 +51,5 @@ export function unregisterEscHotkey(): void {
export function notifyExpectedEscape(): void { export function notifyExpectedEscape(): void {
if (!registered) return if (!registered) return
(requireComputerUseSwift() as any).hotkey?.notifyExpectedEscape() requireComputerUseSwift().hotkey?.notifyExpectedEscape()
} }

View File

@ -40,6 +40,7 @@ import { CLI_CU_CAPABILITIES, CLI_HOST_BUNDLE_ID } from './common.js'
import { validateHwnd } from './win32/shared.js' import { validateHwnd } from './win32/shared.js'
import { loadPlatform } from './platforms/index.js' import { loadPlatform } from './platforms/index.js'
import type { Platform } from './platforms/index.js' import type { Platform } from './platforms/index.js'
import type { WindowAction } from './platforms/types.js'
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helpers for HWND-bound mode // Helpers for HWND-bound mode
@ -62,6 +63,19 @@ function isBound(): boolean {
return getBoundHwndStr() !== null 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). * Get the non-client area offset (title bar height, left border width).
* Returns {dx, dy} to subtract from window coords client coords. * 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}`, `[computer-use] cross-platform executor for ${process.platform}`,
) )
return { const executor: ComputerExecutor = {
capabilities: { capabilities: {
...CLI_CU_CAPABILITIES, ...CLI_CU_CAPABILITIES,
hostBundleId: CLI_HOST_BUNDLE_ID, 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) await sleep(16)
} }
// mouseDown // mouseDown
await (this as any).mouseDown() await executor.mouseDown()
await sleep(50) await sleep(50)
await platform.input.moveMouse(to.x, to.y) await platform.input.moveMouse(to.x, to.y)
await sleep(16) await sleep(16)
// mouseUp // mouseUp
await (this as any).mouseUp() await executor.mouseUp()
}, },
async scroll(x: number, y: number, dx: number, dy: number): Promise<void> { async scroll(x: number, y: number, dx: number, dy: number): Promise<void> {
@ -553,7 +567,8 @@ $i = New-Object MUp+INPUT; $i.type=0; $i.mi.dwFlags=0x0004; [MUp]::SendInput(1,
// ── Window management (Windows only) ────────────────────────────── // ── Window management (Windows only) ──────────────────────────────
async manageWindow(action: string, opts?): Promise<boolean> { async manageWindow(action: string, opts?): Promise<boolean> {
if (!platform.windowManagement) return false 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 // Invalidate NC offset cache on window state change
_ncOffset = null _ncOffset = null
_ncOffsetHwnd = null _ncOffsetHwnd = null
@ -1138,6 +1153,7 @@ $i = New-Object MUp+INPUT; $i.type=0; $i.mi.dwFlags=0x0004; [MUp]::SendInput(1,
} }
}, },
} }
return executor
} }
/** /**

View File

@ -36,7 +36,11 @@ class DebugLogger implements Logger {
function checkAccessibilityJXA(): boolean { function checkAccessibilityJXA(): boolean {
try { try {
const result = Bun.spawnSync({ 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', stdout: 'pipe',
stderr: 'pipe', stderr: 'pipe',
}) })
@ -80,7 +84,7 @@ export function getComputerUseHostAdapter(): ComputerUseHostAdapter {
ensureOsPermissions: async () => { ensureOsPermissions: async () => {
if (process.platform !== 'darwin') return { granted: true } if (process.platform !== 'darwin') return { granted: true }
const cu = requireComputerUseSwift() const cu = requireComputerUseSwift()
const tcc = (cu as any).tcc const tcc = cu.tcc
// Native Swift .node module provides tcc.checkAccessibility/checkScreenRecording. // Native Swift .node module provides tcc.checkAccessibility/checkScreenRecording.
// When absent (decompiled/reverse-engineered build), fall back to JXA probes. // When absent (decompiled/reverse-engineered build), fall back to JXA probes.
if (tcc) { if (tcc) {

View File

@ -1,19 +1,32 @@
import type { ComputerUseAPI } from '@ant/computer-use-swift' 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. * macOS-only loader for @ant/computer-use-swift.
* Non-darwin platforms should use src/utils/computerUse/platforms/ instead. * Non-darwin platforms should use src/utils/computerUse/platforms/ instead.
*/ */
export function requireComputerUseSwift(): ComputerUseAPI { export function requireComputerUseSwift(): ComputerUseSwiftAPI {
if (cached) return cached if (cached) return cached
// eslint-disable-next-line @typescript-eslint/no-require-imports // eslint-disable-next-line @typescript-eslint/no-require-imports
const mod = require('@ant/computer-use-swift') const mod = require('@ant/computer-use-swift')
if (mod.ComputerUseAPI && typeof mod.ComputerUseAPI === 'function') { if (mod.ComputerUseAPI && typeof mod.ComputerUseAPI === 'function') {
cached = new mod.ComputerUseAPI() as ComputerUseAPI cached = new mod.ComputerUseAPI() as ComputerUseSwiftAPI
} else { } else {
cached = mod as ComputerUseAPI cached = mod as ComputerUseSwiftAPI
} }
return cached return cached
} }

View File

@ -23,6 +23,10 @@ interface BridgeResponse {
error?: string error?: string
} }
type FlushableWritable = Writable & {
flush?: () => void
}
let bridgeProc: ReturnType<typeof Bun.spawn> | null = null let bridgeProc: ReturnType<typeof Bun.spawn> | null = null
let requestId = 0 let requestId = 0
const pendingRequests = new Map< const pendingRequests = new Map<
@ -131,10 +135,10 @@ export async function call<T = unknown>(
try { try {
const stdin = bridgeProc!.stdin const stdin = bridgeProc!.stdin
if (stdin) { if (stdin) {
const writable = stdin as unknown as Writable const writable = stdin as unknown as FlushableWritable
writable.write(JSON.stringify(req) + '\n') writable.write(JSON.stringify(req) + '\n')
if (typeof (writable as any).flush === 'function') { if (typeof writable.flush === 'function') {
(writable as any).flush() writable.flush()
} }
} }
} catch (err) { } catch (err) {

View File

@ -8,11 +8,17 @@
*/ */
import * as fs from 'fs/promises' import * as fs from 'fs/promises'
import type { Dirent } from 'fs'
import * as path from 'path' import * as path from 'path'
import { logForDebugging } from '../debug.js' import { logForDebugging } from '../debug.js'
import type { EnvironmentKind } from '../teleport/environments.js' import type { EnvironmentKind } from '../teleport/environments.js'
import type { TurnStartTime } from './types.js' import type { TurnStartTime } from './types.js'
type RecursiveDirent = Dirent & {
parentPath?: string
path?: string
}
/** Shared debug logger for file persistence modules */ /** Shared debug logger for file persistence modules */
export function logDebug(message: string): void { export function logDebug(message: string): void {
logForDebugging(`[file-persistence] ${message}`) logForDebugging(`[file-persistence] ${message}`)
@ -63,13 +69,14 @@ export async function findModifiedFiles(
turnStartTime: TurnStartTime, turnStartTime: TurnStartTime,
outputsDir: string, outputsDir: string,
): Promise<string[]> { ): Promise<string[]> {
const turnStartMs = turnStartTime.turnStartTime
// Use recursive flag to get all entries in one call // Use recursive flag to get all entries in one call
let entries: Awaited<ReturnType<typeof fs.readdir>> | any[] let entries: RecursiveDirent[]
try { try {
entries = await fs.readdir(outputsDir, { entries = await fs.readdir(outputsDir, {
withFileTypes: true, withFileTypes: true,
recursive: true, recursive: true,
}) as any[] })
} catch { } catch {
// Directory doesn't exist or is not accessible // Directory doesn't exist or is not accessible
return [] return []
@ -113,7 +120,7 @@ export async function findModifiedFiles(
// Filter to files modified since turn start // Filter to files modified since turn start
const modifiedFiles: string[] = [] const modifiedFiles: string[] = []
for (const result of statResults) { for (const result of statResults) {
if (result && result.mtimeMs >= (turnStartTime as any as number)) { if (result && result.mtimeMs >= turnStartMs) {
modifiedFiles.push(result.filePath) modifiedFiles.push(result.filePath)
} }
} }

View File

@ -1,5 +1,6 @@
import { feature } from 'bun:bundle' import { feature } from 'bun:bundle'
import type { BetaMessageStreamParams } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' 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 { readdir, readFile, stat } from 'fs/promises'
import memoize from 'lodash-es/memoize.js' import memoize from 'lodash-es/memoize.js'
import { join } from 'path' import { join } from 'path'
@ -228,9 +229,9 @@ export async function getErrorLogByIndex(
* @private * @private
*/ */
async function loadLogList(path: string): Promise<LogOption[]> { async function loadLogList(path: string): Promise<LogOption[]> {
let files: Awaited<ReturnType<typeof readdir>> let files: Dirent<string>[]
try { try {
files = (await readdir(path, { withFileTypes: true })) as any files = await readdir(path, { withFileTypes: true })
} catch { } catch {
logError(new Error(`No logs found at ${path}`)) logError(new Error(`No logs found at ${path}`))
return [] return []

View File

@ -25,6 +25,14 @@ const measures = new Map<
{ name: string; startTime: number; duration: number } { name: string; startTime: number; duration: number }
>() >()
declare global {
var __performanceShimInstalled: boolean | undefined
}
type PerformanceWithUndiciResourceTiming = Performance & {
markResourceTiming(...args: unknown[]): void
}
function now(): number { function now(): number {
return original.now() return original.now()
} }
@ -124,7 +132,10 @@ function clearMeasures(name?: string): void {
// Plain object shim — must NOT inherit from Performance.prototype because // Plain object shim — must NOT inherit from Performance.prototype because
// native getters (onresourcetimingbufferfull, timeOrigin, toJSON) check // native getters (onresourcetimingbufferfull, timeOrigin, toJSON) check
// that `this` is an actual JSC Performance instance and throw otherwise. // that `this` is an actual JSC Performance instance and throw otherwise.
const shim = { const originalWithResourceTiming =
original as PerformanceWithUndiciResourceTiming
const shim: PerformanceWithUndiciResourceTiming = {
now, now,
mark, mark,
measure: measure as typeof performance.measure, measure: measure as typeof performance.measure,
@ -137,21 +148,21 @@ const shim = {
(() => {}) as typeof performance.setResourceTimingBufferSize, (() => {}) as typeof performance.setResourceTimingBufferSize,
// Node.js v22 undici internal calls this after every fetch — must exist to // Node.js v22 undici internal calls this after every fetch — must exist to
// avoid TypeError: markResourceTiming is not a function // avoid TypeError: markResourceTiming is not a function
markResourceTiming: (() => {}) as any, markResourceTiming: () => {},
// Delegate read-only properties to the original // Delegate read-only properties to the original
get timeOrigin() { get timeOrigin() {
return original.timeOrigin return original.timeOrigin
}, },
get onresourcetimingbufferfull() { get onresourcetimingbufferfull() {
return (original as any).onresourcetimingbufferfull return originalWithResourceTiming.onresourcetimingbufferfull
}, },
set onresourcetimingbufferfull(_v: any) { set onresourcetimingbufferfull(_v: Performance['onresourcetimingbufferfull']) {
// no-op — prevent accumulation // no-op — prevent accumulation
}, },
toJSON() { toJSON() {
return original.toJSON() return original.toJSON()
}, },
} as unknown as typeof performance } as unknown as PerformanceWithUndiciResourceTiming
/** /**
* Install the shim onto globalThis.performance. Safe to call multiple times. * Install the shim onto globalThis.performance. Safe to call multiple times.
@ -159,8 +170,8 @@ const shim = {
* native Performance reference. * native Performance reference.
*/ */
export function installPerformanceShim(): void { export function installPerformanceShim(): void {
if ((globalThis as any).__performanceShimInstalled) return if (globalThis.__performanceShimInstalled) return
;(globalThis as any).__performanceShimInstalled = true globalThis.__performanceShimInstalled = true
globalThis.performance = shim globalThis.performance = shim
} }

View File

@ -33,10 +33,10 @@ export function validateInputForSettingsFileEdit(
const updatedContent = getUpdatedContent() const updatedContent = getUpdatedContent()
const afterValidation = validateSettingsFileContent(updatedContent) const afterValidation = validateSettingsFileContent(updatedContent)
if (!afterValidation.isValid) { if (afterValidation.isValid === false) {
return { return {
result: false, 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, errorCode: 10,
} }
} }

View File

@ -16,6 +16,19 @@ const RENDERED_AS_SENTINEL = new Set([
const searchTextCache = new WeakMap<RenderableMessage, string>() const searchTextCache = new WeakMap<RenderableMessage, string>()
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- /** Flatten a RenderableMessage to lowercased searchable text. WeakMap-
* cached messages are append-only and immutable so a hit is always * cached messages are append-only and immutable so a hit is always
* valid. Lowercased at cache time: the only caller immediately * 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 raw = RENDERED_AS_SENTINEL.has(c) ? '' : c
} else { } else {
const parts: string[] = [] const parts: string[] = []
for (const b of (c ?? [])) { for (const b of c ?? []) {
if (b.type === 'text') { if (b.type === 'text') {
if (!RENDERED_AS_SENTINEL.has(b.text)) parts.push(b.text) if (!RENDERED_AS_SENTINEL.has(b.text)) parts.push(b.text)
} else if (b.type === 'tool_result') { } else if (b.type === 'tool_result') {
@ -84,7 +97,9 @@ function computeSearchText(msg: RenderableMessage): string {
// (AttachmentMessage.tsx <Ansi>{m.content}</Ansi>). Visible but // (AttachmentMessage.tsx <Ansi>{m.content}</Ansi>). Visible but
// unsearchable without this — [ dump finds it, / doesn't. // unsearchable without this — [ dump finds it, / doesn't.
if (msg.attachment!.type === 'relevant_memories') { 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 ( } else if (
// Mid-turn prompts — queued while an agent is running. Render via // Mid-turn prompts — queued while an agent is running. Render via
// UserTextMessage (AttachmentMessage.tsx:~348). stickyPromptText // UserTextMessage (AttachmentMessage.tsx:~348). stickyPromptText
@ -97,7 +112,11 @@ function computeSearchText(msg: RenderableMessage): string {
raw = raw =
typeof p === 'string' typeof p === 'string'
? p ? 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 break
} }