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 { export interface ComputerExecutorCapabilities {
screenshotFiltering: 'native' | 'none' screenshotFiltering: 'native' | 'none'
platform: 'darwin' | 'win32' platform: 'darwin' | 'win32' | 'linux'
hostBundleId: string hostBundleId: string
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -78,7 +78,7 @@ export function modelSupportsMaxEffort(_model: string): boolean {
} }
export function modelSupportsXhighEffort(_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) { if (supported3P !== undefined) {
return supported3P return supported3P
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,7 +27,7 @@ import type {
ContextCollapseSnapshotEntry, ContextCollapseSnapshotEntry,
PersistedWorktreeSession, PersistedWorktreeSession,
} from '../types/logs.js' } 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 { renameRecordingForSession } from './asciicast.js'
import { clearMemoryFileCaches } from './claudemd.js' import { clearMemoryFileCaches } from './claudemd.js'
import { import {
@ -78,7 +78,9 @@ function extractTodosFromTranscript(messages: Message[]): TodoList {
for (let i = messages.length - 1; i >= 0; i--) { for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i] const msg = messages[i]
if (msg?.type !== 'assistant') continue 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, block => block.type === 'tool_use' && block.name === TODO_WRITE_TOOL_NAME,
) )
if (!toolUse || toolUse.type !== 'tool_use') continue 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 { Message, SystemAPIErrorMessage } from '../types/message.js'
import { type CacheSafeParams, runForkedAgent } from './forkedAgent.js' import { type CacheSafeParams, runForkedAgent } from './forkedAgent.js'
import { createUserMessage, extractTextContent } from './messages.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) // Pattern to detect "/btw" at start of input (case-insensitive, word boundary)
const BTW_PATTERN = /^\/btw\b/gi const BTW_PATTERN = /^\/btw\b/gi
@ -125,7 +126,12 @@ ${question}`
function extractSideQuestionResponse(messages: Message[]): string | null { function extractSideQuestionResponse(messages: Message[]): string | null {
// Flatten all assistant content blocks across the per-block messages. // Flatten all assistant content blocks across the per-block messages.
const assistantBlocks = messages.flatMap(m => 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) { 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. // No text — check if the model tried to call a tool despite instructions.
const toolUse = assistantBlocks.find(b => b.type === 'tool_use') const toolUse = assistantBlocks.find(b => b.type === 'tool_use')
if (toolUse) { 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.)` 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', m.type === 'system' && 'subtype' in m && m.subtype === 'api_error',
) )
if (apiErr) { 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 return null

View File

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

View File

@ -8,6 +8,12 @@ import {
} from '@alcalzone/ansi-tokenize' } from '@alcalzone/ansi-tokenize'
import type { Theme } from './theme.js' 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 = { export type TextHighlight = {
start: number start: number
end: number end: number
@ -126,19 +132,23 @@ class HighlightSegmenter {
this.codes.push(token) this.codes.push(token)
this.stringPos += token.code.length this.stringPos += token.code.length
this.tokenIdx++ this.tokenIdx++
} else { } else if (isTextToken(token)) {
const tokenValue = token.value
const charsNeeded = targetVisiblePos - this.visiblePos 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) const charsToTake = Math.min(charsNeeded, charsAvailable)
this.stringPos += charsToTake this.stringPos += charsToTake
this.visiblePos += charsToTake this.visiblePos += charsToTake
this.charIdx += charsToTake this.charIdx += charsToTake
if (this.charIdx >= (token as any).value.length) { if (this.charIdx >= tokenValue.length) {
this.tokenIdx++ this.tokenIdx++
this.charIdx = 0 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, worktreeName,
prNumber !== null ? { prNumber } : undefined, prNumber !== null ? { prNumber } : undefined,
) )
if (!result.existed) { if (result.existed === false) {
// biome-ignore lint/suspicious/noConsole: intentional console output // biome-ignore lint/suspicious/noConsole: intentional console output
console.log( console.log(
`Created worktree: ${worktreeDir} (based on ${(result as any).baseBranch})`, `Created worktree: ${worktreeDir} (based on ${result.baseBranch})`,
) )
await performPostCreationSetup(repoRoot, worktreeDir) await performPostCreationSetup(repoRoot, worktreeDir)
} }