fix: remove queued content as-any casts

This commit is contained in:
James Feng 2026-06-07 15:40:32 +08:00
parent c130eaad4b
commit ba36ca454d
16 changed files with 124 additions and 46 deletions

View File

@ -49,7 +49,7 @@ export interface ResolvePrepareCaptureResult extends ScreenshotResult {
export interface ComputerExecutorCapabilities {
screenshotFiltering: 'native' | 'none'
platform: 'darwin' | 'win32'
platform: 'darwin' | 'win32' | 'linux'
hostBundleId: string
}

View File

@ -123,7 +123,7 @@ export function normalizeKeySequence(seq: string): string {
*/
export function isSystemKeyCombo(
seq: string,
platform: 'darwin' | 'win32',
platform: 'darwin' | 'win32' | 'linux',
): boolean {
const blocklist = platform === 'darwin' ? BLOCKED_DARWIN : BLOCKED_WIN32
const { mods, keys } = partitionKeys(seq)

View File

@ -16,7 +16,14 @@ import type {
* Individual message subtypes (UserMessage, AssistantMessage, etc.) extend
* this with narrower `type` literals and additional fields.
*/
export type MessageType = 'user' | 'assistant' | 'system' | 'attachment' | 'progress' | 'grouped_tool_use' | 'collapsed_read_search'
export type MessageType =
| 'user'
| 'assistant'
| 'system'
| 'attachment'
| 'progress'
| 'grouped_tool_use'
| 'collapsed_read_search'
/** A single content element inside message.content arrays. */
export type ContentItem = ContentBlockParam | ContentBlock
@ -37,7 +44,14 @@ export type Message = {
isCompactSummary?: boolean
toolUseResult?: unknown
isVisibleInTranscriptOnly?: boolean
attachment?: { type: string; toolUseID?: string; [key: string]: unknown; addedNames: string[]; addedLines: string[]; removedNames: string[] }
attachment?: {
type: string
toolUseID?: string
[key: string]: unknown
addedNames: string[]
addedLines: string[]
removedNames: string[]
}
message?: {
role?: string
id?: string
@ -52,8 +66,12 @@ export type AssistantMessage = Message & {
type: 'assistant'
message: NonNullable<Message['message']>
}
export type AttachmentMessage<T = { type: string; [key: string]: unknown }> = Message & { type: 'attachment'; attachment: T }
export type ProgressMessage<T = unknown> = Message & { type: 'progress'; data: T }
export type AttachmentMessage<T = { type: string; [key: string]: unknown }> =
Message & { type: 'attachment'; attachment: T }
export type ProgressMessage<T = unknown> = Message & {
type: 'progress'
data: T
}
export type SystemLocalCommandMessage = Message & { type: 'system' }
export type SystemMessage = Message & { type: 'system' }
export type UserMessage = Message & {
@ -78,7 +96,7 @@ export type SystemCompactBoundaryMessage = Message & {
}
export type TombstoneMessage = Message
export type ToolUseSummaryMessage = Message
export type MessageOrigin = string
export type MessageOrigin = string | { kind: string; [key: string]: unknown }
export type CompactMetadata = Record<string, unknown>
export type SystemAPIErrorMessage = Message & { type: 'system' }
export type SystemFileSnapshotMessage = Message & { type: 'system' }
@ -126,7 +144,14 @@ export type RenderableMessage =
| AssistantMessage
| UserMessage
| (Message & { type: 'system' })
| (Message & { type: 'attachment'; attachment: { type: string; memories?: { path: string; content: string; mtimeMs: number }[]; [key: string]: unknown } })
| (Message & {
type: 'attachment'
attachment: {
type: string
memories?: { path: string; content: string; mtimeMs: number }[]
[key: string]: unknown
}
})
| (Message & { type: 'progress' })
| GroupedToolUseMessage
| CollapsedReadSearchGroup

View File

@ -3,6 +3,11 @@ import { env } from '../env.js'
export const COMPUTER_USE_MCP_SERVER_NAME = 'computer-use'
type CliComputerUseCapabilities = {
screenshotFiltering: 'native' | 'none'
platform: 'darwin' | 'win32' | 'linux'
}
/**
* Sentinel bundle ID for the frontmost gate. Claude Code is a terminal it has
* no window. This never matches a real `NSWorkspace.frontmostApplication`, so
@ -52,15 +57,14 @@ export function getTerminalBundleId(): string | null {
* takes this shape (no `hostBundleId`, no `teachMode`).
*/
export const CLI_CU_CAPABILITIES = {
screenshotFiltering: (process.platform === 'darwin'
? 'native'
: 'none') as any,
platform: (process.platform === 'win32'
? 'win32'
: process.platform === 'linux'
? 'linux'
: 'darwin') as any,
}
screenshotFiltering: process.platform === 'darwin' ? 'native' : 'none',
platform:
process.platform === 'win32'
? 'win32'
: process.platform === 'linux'
? 'linux'
: 'darwin',
} satisfies CliComputerUseCapabilities
export function isComputerUseMCPServer(name: string): boolean {
return normalizeNameForMCP(name) === COMPUTER_USE_MCP_SERVER_NAME

View File

@ -94,11 +94,11 @@ export async function handleUrlSchemeLaunch(): Promise<number | null> {
try {
const { waitForUrlEvent } = await import('url-handler-napi')
const url = (waitForUrlEvent as any)(5000)
const url = await waitForUrlEvent(5000)
if (!url) {
return null
}
return await handleDeepLinkUri(await url as string)
return await handleDeepLinkUri(url)
} catch {
// NAPI module not available, or handleDeepLinkUri rejected — not a URL launch
return null

View File

@ -78,7 +78,7 @@ export function modelSupportsMaxEffort(_model: string): boolean {
}
export function modelSupportsXhighEffort(_model: string): boolean {
const supported3P = get3PModelCapabilityOverride(_model, 'xhigh_effort' as any)
const supported3P = get3PModelCapabilityOverride(_model, 'xhigh_effort')
if (supported3P !== undefined) {
return supported3P
}

View File

@ -368,7 +368,8 @@ export function isQueuedCommandEditable(cmd: QueuedCommand): boolean {
export function isQueuedCommandVisible(cmd: QueuedCommand): boolean {
if (
(feature('KAIROS') || feature('KAIROS_CHANNELS')) &&
(cmd as any).origin?.kind === 'channel'
typeof cmd.origin === 'object' &&
cmd.origin?.kind === 'channel'
)
return true
return isQueuedCommandEditable(cmd)

View File

@ -4,6 +4,7 @@ import { getAPIProvider } from './providers.js'
export type ModelCapabilityOverride =
| 'effort'
| 'max_effort'
| 'xhigh_effort'
| 'thinking'
| 'adaptive_thinking'
| 'interleaved_thinking'

View File

@ -362,19 +362,24 @@ export async function persistFileSnapshotIfRemote(): Promise<void> {
return
}
try {
const snapshotFiles: SystemFileSnapshotMessage['snapshotFiles'] = []
type SnapshotFile = {
key: string
path: string
content: string
}
const snapshotFiles: SnapshotFile[] = []
// Snapshot plan file
const plan = getPlan()
if (plan) {
(snapshotFiles as any[]).push({
snapshotFiles.push({
key: 'plan',
path: getPlanFilePath(),
content: plan,
})
}
if ((snapshotFiles as any[]).length === 0) {
if (snapshotFiles.length === 0) {
return
}

View File

@ -184,11 +184,15 @@ export function getDeclaredMarketplaces(): Record<string, DeclaredMarketplace> {
// Lowest precedence: implicit < --add-dir < merged settings.
// An explicit extraKnownMarketplaces entry for claude-plugins-official
// in --add-dir or settings wins.
return {
const declared = {
...implicit,
...getAddDirExtraMarketplaces(),
...(getInitialSettings().extraKnownMarketplaces ?? {}),
} as any
...(getAddDirExtraMarketplaces() as Record<string, DeclaredMarketplace>),
...((getInitialSettings().extraKnownMarketplaces ?? {}) as Record<
string,
DeclaredMarketplace
>),
} satisfies Record<string, DeclaredMarketplace>
return declared
}
/**

View File

@ -24,7 +24,7 @@ import {
isDeferredTool,
SEARCH_EXTRA_TOOLS_TOOL_NAME,
} from '@claude-code-best/builtin-tools/tools/SearchExtraToolsTool/prompt.js'
import type { Message } from '../types/message.js'
import type { Message, SystemCompactBoundaryMessage } from '../types/message.js'
import {
countToolDefinitionTokens,
TOOL_TOKEN_COUNT_OVERHEAD,
@ -504,12 +504,15 @@ export function extractDiscoveredToolNames(messages: Message[]): Set<string> {
// check rather than isCompactBoundaryMessage — utils/messages.ts imports
// from this file, so importing back would be circular.
if (msg.type === 'system' && msg.subtype === 'compact_boundary') {
const carried = (msg as any).compactMetadata?.preCompactDiscoveredTools as
| string[]
| undefined
const boundary = msg as SystemCompactBoundaryMessage
const carried = boundary.compactMetadata.preCompactDiscoveredTools
if (carried) {
for (const name of carried) discoveredTools.add(name)
carriedFromBoundary += carried.length
if (Array.isArray(carried)) {
for (const name of carried) {
if (typeof name === 'string') discoveredTools.add(name)
}
carriedFromBoundary += carried.length
}
}
continue
}

View File

@ -27,7 +27,7 @@ import type {
ContextCollapseSnapshotEntry,
PersistedWorktreeSession,
} from '../types/logs.js'
import type { Message } from '../types/message.js'
import type { ContentItem, Message } from '../types/message.js'
import { renameRecordingForSession } from './asciicast.js'
import { clearMemoryFileCaches } from './claudemd.js'
import {
@ -78,7 +78,9 @@ function extractTodosFromTranscript(messages: Message[]): TodoList {
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i]
if (msg?.type !== 'assistant') continue
const toolUse = (msg.message!.content as any[]).find(
const content = msg.message?.content
if (!Array.isArray(content)) continue
const toolUse = (content as ContentItem[]).find(
block => block.type === 'tool_use' && block.name === TODO_WRITE_TOOL_NAME,
)
if (!toolUse || toolUse.type !== 'tool_use') continue

View File

@ -11,6 +11,7 @@ import type { NonNullableUsage } from '../services/api/logging.js'
import type { Message, SystemAPIErrorMessage } from '../types/message.js'
import { type CacheSafeParams, runForkedAgent } from './forkedAgent.js'
import { createUserMessage, extractTextContent } from './messages.js'
import type { APIError } from '@anthropic-ai/sdk'
// Pattern to detect "/btw" at start of input (case-insensitive, word boundary)
const BTW_PATTERN = /^\/btw\b/gi
@ -125,7 +126,12 @@ ${question}`
function extractSideQuestionResponse(messages: Message[]): string | null {
// Flatten all assistant content blocks across the per-block messages.
const assistantBlocks = messages.flatMap(m =>
m.type === 'assistant' ? (m.message!.content as unknown as Array<{ type: string; [key: string]: unknown }>) : [],
m.type === 'assistant'
? (m.message!.content as unknown as Array<{
type: string
[key: string]: unknown
}>)
: [],
)
if (assistantBlocks.length > 0) {
@ -136,7 +142,8 @@ function extractSideQuestionResponse(messages: Message[]): string | null {
// No text — check if the model tried to call a tool despite instructions.
const toolUse = assistantBlocks.find(b => b.type === 'tool_use')
if (toolUse) {
const toolName = 'name' in toolUse ? (toolUse as any).name : 'a tool'
const toolName =
typeof toolUse.name === 'string' ? toolUse.name : 'a tool'
return `(The model tried to call ${toolName} instead of answering directly. Try rephrasing or ask in the main conversation.)`
}
}
@ -148,7 +155,11 @@ function extractSideQuestionResponse(messages: Message[]): string | null {
m.type === 'system' && 'subtype' in m && m.subtype === 'api_error',
)
if (apiErr) {
return `(API error: ${formatAPIError(apiErr.error as any)})`
const error =
apiErr.error instanceof Error
? apiErr.error
: new Error(String(apiErr.error))
return `(API error: ${formatAPIError(error as APIError)})`
}
return null

View File

@ -1,7 +1,9 @@
import {
type AnsiCode,
type Char,
ansiCodesToString,
reduceAnsiCodes,
type Token,
tokenize,
undoAnsiCodes,
} from '@alcalzone/ansi-tokenize'
@ -17,6 +19,10 @@ function filterStartCodes(codes: AnsiCode[]): AnsiCode[] {
return codes.filter(c => !isEndCode(c))
}
function isCharToken(token: Token): token is Char {
return token.type === 'char'
}
/**
* Slice a string containing ANSI escape codes.
*
@ -43,7 +49,13 @@ export default function sliceAnsi(
// pass start/end in display cells (via stringWidth), so position must
// track the same units.
const width =
token.type === 'ansi' ? 0 : token.type === 'char' ? (token.fullWidth ? 2 : stringWidth(token.value)) : 0
token.type === 'ansi'
? 0
: token.type === 'char'
? token.fullWidth
? 2
: stringWidth(token.value)
: 0
// Break AFTER trailing zero-width marks — a combining mark attaches to
// the preceding base char, so "भा" (भ + ा, 1 display cell) sliced at
@ -77,7 +89,7 @@ export default function sliceAnsi(
}
if (include) {
result += (token as any).value
result += isCharToken(token) ? token.value : token.code
}
position += width

View File

@ -8,6 +8,12 @@ import {
} from '@alcalzone/ansi-tokenize'
import type { Theme } from './theme.js'
type TextToken = Extract<Token, { type: 'char' }>
function isTextToken(token: Token): token is TextToken {
return token.type === 'char'
}
export type TextHighlight = {
start: number
end: number
@ -126,19 +132,23 @@ class HighlightSegmenter {
this.codes.push(token)
this.stringPos += token.code.length
this.tokenIdx++
} else {
} else if (isTextToken(token)) {
const tokenValue = token.value
const charsNeeded = targetVisiblePos - this.visiblePos
const charsAvailable = (token as any).value.length - this.charIdx
const charsAvailable = tokenValue.length - this.charIdx
const charsToTake = Math.min(charsNeeded, charsAvailable)
this.stringPos += charsToTake
this.visiblePos += charsToTake
this.charIdx += charsToTake
if (this.charIdx >= (token as any).value.length) {
if (this.charIdx >= tokenValue.length) {
this.tokenIdx++
this.charIdx = 0
}
} else {
this.stringPos += token.code.length
this.tokenIdx++
}
}

View File

@ -1290,10 +1290,10 @@ export async function execIntoTmuxWorktree(args: string[]): Promise<{
worktreeName,
prNumber !== null ? { prNumber } : undefined,
)
if (!result.existed) {
if (result.existed === false) {
// biome-ignore lint/suspicious/noConsole: intentional console output
console.log(
`Created worktree: ${worktreeDir} (based on ${(result as any).baseBranch})`,
`Created worktree: ${worktreeDir} (based on ${result.baseBranch})`,
)
await performPostCreationSetup(repoRoot, worktreeDir)
}