feat: 全部类型问题解决

This commit is contained in:
claude-code-best
2026-04-11 10:24:00 +08:00
parent 7088fe3c8b
commit 6a70056910
135 changed files with 671 additions and 503 deletions

View File

@@ -86,9 +86,9 @@ describe("createAssistantMessage", () => {
test("creates assistant message with string content", () => {
const msg = createAssistantMessage({ content: "hello" });
expect(msg.type).toBe("assistant");
expect(msg.message.role).toBe("assistant");
expect(msg.message.content).toHaveLength(1);
expect((msg.message.content[0] as any).text).toBe("hello");
expect(msg.message!.role).toBe("assistant");
expect(msg.message!.content![0] as any).toBeTruthy();
expect((msg.message!.content![0] as any).text).toBe("hello");
});
test("creates assistant message with content blocks", () => {
@@ -501,7 +501,7 @@ describe("normalizeMessagesForAPI", () => {
]);
const normalized = normalizeMessagesForAPI([assistant]);
const block = (normalized[0] as AssistantMessage).message.content[0] as any;
const block = (normalized[0] as AssistantMessage).message!.content![0] as any;
expect(block.type).toBe("tool_use");
expect(block._geminiThoughtSignature).toBe("sig-123");

View File

@@ -445,8 +445,8 @@ async function countBuiltInToolTokens(
if (messages) {
const deferredToolNameSet = new Set(deferredBuiltinTools.map(t => t.name))
for (const msg of messages) {
if (msg.type === 'assistant' && Array.isArray(msg.message.content)) {
for (const block of msg.message.content) {
if (msg.type === 'assistant' && Array.isArray(msg.message!.content)) {
for (const block of msg.message!.content) {
if (
typeof block !== 'string' &&
'type' in block &&
@@ -683,8 +683,8 @@ export async function countMcpToolTokens(
if (isDeferred && messages) {
const mcpToolNameSet = new Set(mcpTools.map(t => t.name))
for (const msg of messages) {
if (msg.type === 'assistant' && Array.isArray(msg.message.content)) {
for (const block of msg.message.content) {
if (msg.type === 'assistant' && Array.isArray(msg.message!.content)) {
for (const block of msg.message!.content) {
if (
typeof block !== 'string' &&
'type' in block &&
@@ -786,7 +786,7 @@ function processAssistantMessage(
breakdown: MessageBreakdown,
): void {
// Process each content block individually
const contentBlocks = Array.isArray(msg.message.content) ? msg.message.content : []
const contentBlocks = Array.isArray(msg.message!.content) ? msg.message!.content : []
for (const block of contentBlocks) {
const blockStr = jsonStringify(block)
const blockTokens = roughTokenCountEstimation(blockStr)
@@ -811,20 +811,19 @@ function processUserMessage(
toolUseIdToName: Map<string, string>,
): void {
// Handle both string and array content
if (typeof msg.message.content === 'string') {
if (typeof msg.message!.content === 'string') {
// Simple string content
const tokens = roughTokenCountEstimation(msg.message.content)
const tokens = roughTokenCountEstimation(msg.message!.content)
breakdown.userMessageTokens += tokens
return
}
// Process each content block individually
for (const block of msg.message.content) {
for (const block of (msg.message!.content ?? [])) {
const blockStr = jsonStringify(block)
const blockTokens = roughTokenCountEstimation(blockStr)
if ('type' in block && block.type === 'tool_result') {
breakdown.toolResultTokens += blockTokens
const toolUseId = 'tool_use_id' in block ? block.tool_use_id : undefined
const toolName =
(toolUseId ? toolUseIdToName.get(toolUseId) : undefined) || 'unknown'
@@ -874,8 +873,8 @@ async function approximateMessageTokens(
// Build a map of tool_use_id to tool_name for easier lookup
const toolUseIdToName = new Map<string, string>()
for (const msg of microcompactResult.messages) {
if (msg.type === 'assistant' && Array.isArray(msg.message.content)) {
for (const block of msg.message.content) {
if (msg.type === 'assistant' && Array.isArray(msg.message!.content)) {
for (const block of msg.message!.content) {
if (typeof block !== 'string' && 'type' in block && block.type === 'tool_use') {
const toolUseId = 'id' in block ? (block.id as string) : undefined
const toolName =

View File

@@ -193,8 +193,8 @@ export function installAsciicastRecorder(): void {
) as typeof process.stdout.write
process.stdout.write = function (
chunk: string | Uint8Array,
encodingOrCb?: BufferEncoding | ((err?: Error) => void),
cb?: (err?: Error) => void,
encodingOrCb?: BufferEncoding | ((err?: Error | null) => void),
cb?: (err?: Error | null) => void,
): boolean {
// Record the output event
const elapsed = (performance.now() - startTime) / 1000

View File

@@ -1147,13 +1147,13 @@ function getPlanModeAttachmentTurnCount(messages: Message[]): {
if (
message?.type === 'user' &&
!message.isMeta &&
!hasToolResultContent(message.message.content)
!hasToolResultContent(message.message!.content)
) {
turnsSinceLastAttachment++
} else if (
message?.type === 'attachment' &&
(message.attachment.type === 'plan_mode' ||
message.attachment.type === 'plan_mode_reentry')
(message.attachment!.type === 'plan_mode' ||
message.attachment!.type === 'plan_mode_reentry')
) {
foundPlanModeAttachment = true
break
@@ -1173,10 +1173,10 @@ function countPlanModeAttachmentsSinceLastExit(messages: Message[]): number {
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i]
if (message?.type === 'attachment') {
if (message.attachment.type === 'plan_mode_exit') {
if (message.attachment!.type === 'plan_mode_exit') {
break // Stop counting at the last exit
}
if (message.attachment.type === 'plan_mode') {
if (message.attachment!.type === 'plan_mode') {
count++
}
}
@@ -1292,18 +1292,18 @@ function getAutoModeAttachmentTurnCount(messages: Message[]): {
if (
message?.type === 'user' &&
!message.isMeta &&
!hasToolResultContent(message.message.content)
!hasToolResultContent(message.message!.content)
) {
turnsSinceLastAttachment++
} else if (
message?.type === 'attachment' &&
message.attachment.type === 'auto_mode'
message.attachment!.type === 'auto_mode'
) {
foundAutoModeAttachment = true
break
} else if (
message?.type === 'attachment' &&
message.attachment.type === 'auto_mode_exit'
message.attachment!.type === 'auto_mode_exit'
) {
// Exit resets the throttle — treat as if no prior attachment exists
break
@@ -1322,10 +1322,10 @@ function countAutoModeAttachmentsSinceLastExit(messages: Message[]): number {
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i]
if (message?.type === 'attachment') {
if (message.attachment.type === 'auto_mode_exit') {
if (message.attachment!.type === 'auto_mode_exit') {
break
}
if (message.attachment.type === 'auto_mode') {
if (message.attachment!.type === 'auto_mode') {
count++
}
}
@@ -1525,9 +1525,9 @@ export function getAgentListingDeltaAttachment(
const announced = new Set<string>()
for (const msg of messages ?? []) {
if (msg.type !== 'attachment') continue
if (msg.attachment.type !== 'agent_listing_delta') continue
for (const t of msg.attachment.addedTypes as string[]) announced.add(t)
for (const t of msg.attachment.removedTypes as string[]) announced.delete(t)
if (msg.attachment!.type !== 'agent_listing_delta') continue
for (const t of msg.attachment!.addedTypes as string[]) announced.add(t)
for (const t of msg.attachment!.removedTypes as string[]) announced.delete(t)
}
const currentTypes = new Set(filtered.map(a => a.agentType))
@@ -2256,8 +2256,8 @@ export function collectSurfacedMemories(messages: ReadonlyArray<Message>): {
const paths = new Set<string>()
let totalBytes = 0
for (const m of messages) {
if (m.type === 'attachment' && m.attachment.type === 'relevant_memories') {
for (const mem of m.attachment.memories as { path: string; content: string; mtimeMs: number }[]) {
if (m.type === 'attachment' && m.attachment!.type === 'relevant_memories') {
for (const mem of m.attachment!.memories as { path: string; content: string; mtimeMs: number }[]) {
paths.add(mem.path)
totalBytes += mem.content.length
}
@@ -2473,16 +2473,16 @@ export function collectRecentSuccessfulTools(
const m = messages[i]
if (!m) continue
if (isHumanTurn(m) && m !== lastUserMessage) break
if (m.type === 'assistant' && typeof m.message.content !== 'string') {
for (const block of m.message.content) {
if (m.type === 'assistant' && typeof m.message!.content !== 'string') {
for (const block of m.message!.content as Array<{type: string; id: string; name: string}>) {
if (block.type === 'tool_use') useIdToName.set(block.id, block.name)
}
} else if (
m.type === 'user' &&
'message' in m &&
Array.isArray(m.message.content)
Array.isArray(m.message!.content)
) {
for (const block of m.message.content) {
for (const block of m.message!.content as Array<{type: string}>) {
if (isToolResultBlock(block)) {
resultByUseId.set(block.tool_use_id, block.is_error === true)
}
@@ -3201,13 +3201,13 @@ export async function generateFileAttachment(
export function createAttachmentMessage(
attachment: Attachment,
): AttachmentMessage {
): AttachmentMessage<Attachment> {
return {
attachment,
type: 'attachment',
uuid: randomUUID(),
timestamp: new Date().toISOString(),
}
} as unknown as AttachmentMessage<Attachment>
}
function getTodoReminderTurnCounts(messages: Message[]): {
@@ -3248,7 +3248,7 @@ function getTodoReminderTurnCounts(messages: Message[]): {
} else if (
lastReminderIndex === -1 &&
message?.type === 'attachment' &&
message.attachment.type === 'todo_reminder'
message.attachment!.type === 'todo_reminder'
) {
lastReminderIndex = i
}
@@ -3357,7 +3357,7 @@ function getTaskReminderTurnCounts(messages: Message[]): {
} else if (
lastReminderIndex === -1 &&
message?.type === 'attachment' &&
message.attachment.type === 'task_reminder'
message.attachment!.type === 'task_reminder'
) {
lastReminderIndex = i
}
@@ -3880,7 +3880,7 @@ export function getVerifyPlanReminderTurnCount(messages: Message[]): number {
// Stop counting at plan_mode_exit attachment (marks when implementation started)
if (
message?.type === 'attachment' &&
message.attachment.type === 'plan_mode_exit'
message.attachment!.type === 'plan_mode_exit'
) {
return turnCount
}

View File

@@ -940,7 +940,7 @@ export function collapseReadSearchGroups(
// suppresses the fallback). createCollapsedGroup adds .length to
// memoryReadCount after the readCount subtraction instead.
currentGroup.relevantMemories ??= []
currentGroup.relevantMemories.push(...msg.attachment.memories)
currentGroup.relevantMemories.push(...(msg.attachment.memories ?? []))
} else if (shouldSkipMessage(msg)) {
// Don't flush the group for skippable messages (thinking, attachments, system)
// If a group is in progress, defer these messages to output after the collapsed group

View File

@@ -43,7 +43,7 @@ export function collapseTeammateShutdowns(
type: 'teammate_shutdown_batch',
count,
},
})
} as unknown as RenderableMessage)
}
} else {
result.push(msg)

View File

@@ -394,10 +394,10 @@ public class WScroll {
// ---------------------------------------------------------------------------
const screenshot: ScreenshotPlatform = {
async captureScreen(displayId) {
async captureScreen(displayId): Promise<ScreenshotResult> {
// If HWND is bound, capture that specific window
if (boundHwnd) {
const result = this.captureWindow?.(String(boundHwnd))
const result = await this.captureWindow?.(String(boundHwnd))
if (result) return result
}
@@ -415,10 +415,10 @@ const screenshot: ScreenshotPlatform = {
)
},
async captureRegion(x, y, w, h) {
async captureRegion(x, y, w, h): Promise<ScreenshotResult> {
// When HWND is bound, the window IS the region (matches macOS behavior)
if (boundHwnd) {
const result = this.captureWindow?.(String(boundHwnd))
const result = await this.captureWindow?.(String(boundHwnd))
if (result) return result
}
return this.captureScreen()

View File

@@ -46,14 +46,14 @@ export function analyzeContext(messages: Message[]): TokenStats {
messages.forEach(msg => {
if (msg.type === 'attachment') {
const type = msg.attachment.type || 'unknown'
const type = msg.attachment!.type || 'unknown'
stats.attachments.set(type, (stats.attachments.get(type) || 0) + 1)
}
})
const normalizedMessages = normalizeMessagesForAPI(messages)
normalizedMessages.forEach(msg => {
const { content } = msg.message
const { content } = msg.message!
// Not sure if this path is still used, but adding as a fallback
if (typeof content === 'string') {
@@ -67,7 +67,7 @@ export function analyzeContext(messages: Message[]): TokenStats {
tokens
}
} else {
content.forEach(block =>
content!.forEach(block =>
processBlock(
block,
msg,

View File

@@ -124,7 +124,7 @@ function migrateLegacyAttachmentTypes(message: Message): Message {
...attachment,
displayPath: relative(getCwd(), path),
},
} as Message
} as unknown as Message
}
}
@@ -359,7 +359,7 @@ function isTerminalToolResult(
for (let i = resultIdx - 1; i >= 0; i--) {
const msg = messages[i]!
if (msg.type !== 'assistant') continue
const msgContent = msg.message.content
const msgContent = msg.message!.content
if (!Array.isArray(msgContent)) continue
for (const b of msgContent) {
if (typeof b !== 'string' && 'type' in b && b.type === 'tool_use' && 'id' in b && b.id === toolUseId) {
@@ -386,8 +386,8 @@ export function restoreSkillStateFromMessages(messages: Message[]): void {
if (message.type !== 'attachment') {
continue
}
if (message.attachment.type === 'invoked_skills') {
const skills = message.attachment.skills as Array<{ name?: string; path?: string; content?: string }>;
if (message.attachment!.type === 'invoked_skills') {
const skills = message.attachment!.skills as Array<{ name?: string; path?: string; content?: string }>;
for (const skill of skills) {
if (skill.name && skill.path && skill.content) {
// Resume only happens for the main session, so agentId is null
@@ -399,7 +399,7 @@ export function restoreSkillStateFromMessages(messages: Message[]): void {
// in the transcript the model is about to see. sentSkillNames is
// process-local, so without this every resume re-announces the same
// ~600 tokens. Fire-once latch; consumed on the first attachment pass.
if (message.attachment.type === 'skill_listing') {
if (message.attachment!.type === 'skill_listing') {
suppressNextSkillListing()
}
}

View File

@@ -46,7 +46,7 @@ function StaticKeybindingProvider({
// AttachmentMessage etc. have no .message and normalize to ≤1.
function normalizedUpperBound(m: Message): number {
if (!('message' in m)) return 1
const c = m.message.content
const c = m.message!.content
return Array.isArray(c) ? c.length : 1
}

View File

@@ -136,7 +136,7 @@ export function applyGrouping(
const results: NormalizedUserMessage[] = []
for (const assistantMsg of group) {
const toolUseId = (
assistantMsg.message.content[0] as { id: string }
assistantMsg.message!.content![0] as { id: string }
).id
const resultMsg = resultsByToolUseId.get(toolUseId)
if (resultMsg) {

View File

@@ -2377,7 +2377,7 @@ async function* executeHooks({
)
// Inject timing fields for hook visibility
if (promptResult.message?.type === 'attachment') {
const att = promptResult.message.attachment
const att = promptResult.message.attachment!
if (
att.type === 'hook_success' ||
att.type === 'hook_non_blocking_error'
@@ -2417,7 +2417,7 @@ async function* executeHooks({
)
// Inject timing fields for hook visibility
if (agentResult.message?.type === 'attachment') {
const att = agentResult.message.attachment
const att = agentResult.message.attachment!
if (
att.type === 'hook_success' ||
att.type === 'hook_non_blocking_error'

View File

@@ -117,7 +117,7 @@ export function createApiQueryHook<TResult>(
type: 'success',
queryName: config.name,
result,
messageId: response.message.id,
messageId: response.message.id ?? '',
model,
uuid,
},

View File

@@ -327,7 +327,7 @@ export function groupHooksByEventAndMatcher(
const eventGroup = grouped[hookEvent]
if (!eventGroup) continue
for (const matcher of matchers) {
for (const matcher of (matchers ?? [])) {
const matcherKey = matcher.matcher || ''
// Only PluginHookMatcher has pluginRoot; HookCallbackMatcher (internal

View File

@@ -41,10 +41,10 @@ function formatRecentMessages(messages: Message[]): string {
.filter(m => m.type === 'user' || m.type === 'assistant')
.map(m => {
const role = m.type === 'user' ? 'User' : 'Assistant'
const content = m.message.content
const content = m.message!.content
if (typeof content === 'string')
return `${role}: ${content.slice(0, 500)}`
const text = content
const text = (content ?? [])
.filter(
(b): b is Extract<typeof b, { type: 'text' }> => b.type === 'text',
)

View File

@@ -63,10 +63,10 @@ export function getMcpInstructionsDelta(
for (const msg of messages) {
if (msg.type !== 'attachment') continue
attachmentCount++
if (msg.attachment.type !== 'mcp_instructions_delta') continue
if (msg.attachment!.type !== 'mcp_instructions_delta') continue
midCount++
for (const n of (msg.attachment as any).addedNames) announced.add(n)
for (const n of (msg.attachment as any).removedNames) announced.delete(n)
for (const n of (msg.attachment! as any).addedNames) announced.add(n)
for (const n of (msg.attachment! as any).removedNames) announced.delete(n)
}
const connected = mcpClients.filter(

View File

@@ -313,9 +313,9 @@ export function isSyntheticMessage(message: Message): boolean {
message.type !== 'progress' &&
message.type !== 'attachment' &&
message.type !== 'system' &&
Array.isArray(message.message.content) &&
message.message.content[0]?.type === 'text' &&
SYNTHETIC_MESSAGES.has(message.message.content[0].text)
Array.isArray(message.message?.content) &&
message.message?.content[0]?.type === 'text' &&
SYNTHETIC_MESSAGES.has((message.message?.content[0] as { text: string }).text)
)
}
@@ -325,7 +325,7 @@ function isSyntheticApiErrorMessage(
return (
message.type === 'assistant' &&
message.isApiErrorMessage === true &&
message.message.model === SYNTHETIC_MODEL
message.message?.model === SYNTHETIC_MODEL
)
}
@@ -696,27 +696,30 @@ export function isNotEmptyMessage(message: Message): boolean {
return true
}
if (typeof message.message.content === 'string') {
return message.message.content.trim().length > 0
const msg = message.message
if (!msg) return true
if (typeof msg.content === 'string') {
return msg.content.trim().length > 0
}
if (message.message.content.length === 0) {
if (!msg.content || msg.content.length === 0) {
return false
}
// Skip multi-block messages for now
if (message.message.content.length > 1) {
if (msg.content.length > 1) {
return true
}
if (message.message.content[0]!.type !== 'text') {
if (msg.content[0]!.type !== 'text') {
return true
}
return (
message.message.content[0]!.text.trim().length > 0 &&
message.message.content[0]!.text !== NO_CONTENT_MESSAGE &&
message.message.content[0]!.text !== INTERRUPT_MESSAGE_FOR_TOOL_USE
(msg.content[0] as { text: string }).text.trim().length > 0 &&
(msg.content[0] as { text: string }).text !== NO_CONTENT_MESSAGE &&
(msg.content[0] as { text: string }).text !== INTERRUPT_MESSAGE_FOR_TOOL_USE
)
}
@@ -750,7 +753,8 @@ export function normalizeMessages(messages: Message[]): NormalizedMessage[] {
return messages.flatMap(message => {
switch (message.type) {
case 'assistant': {
const assistantContent = Array.isArray(message.message.content) ? message.message.content : []
const aMsg = message as AssistantMessage
const assistantContent = Array.isArray(aMsg.message.content) ? aMsg.message.content : []
isNewChain = isNewChain || assistantContent.length > 1
return assistantContent.map((_, index) => {
const uuid = isNewChain
@@ -760,9 +764,9 @@ export function normalizeMessages(messages: Message[]): NormalizedMessage[] {
type: 'assistant' as const,
timestamp: message.timestamp,
message: {
...message.message,
...aMsg.message,
content: [_],
context_management: message.message.context_management ?? null,
context_management: aMsg.message.context_management ?? null,
},
isMeta: message.isMeta,
isVirtual: message.isVirtual,
@@ -781,45 +785,48 @@ export function normalizeMessages(messages: Message[]): NormalizedMessage[] {
case 'system':
return [message]
case 'user': {
if (typeof message.message.content === 'string') {
const uuid = isNewChain ? deriveUUID(message.uuid, 0) : message.uuid
const uMsg = message as UserMessage
if (typeof uMsg.message.content === 'string') {
const uuid = isNewChain ? deriveUUID(uMsg.uuid, 0) : uMsg.uuid
return [
{
...message,
...uMsg,
uuid,
message: {
...message.message,
content: [{ type: 'text', text: message.message.content }],
...uMsg.message,
content: [{ type: 'text', text: uMsg.message.content }],
},
} as NormalizedMessage,
]
}
isNewChain = isNewChain || message.message.content.length > 1
isNewChain = isNewChain || (uMsg.message.content?.length ?? 0) > 1
let imageIndex = 0
return message.message.content.map((_, index) => {
return (uMsg.message.content ?? []).map((_, index) => {
const isImage = _.type === 'image'
// For image content blocks, extract just the ID for this image
const imageId =
isImage && message.imagePasteIds
? message.imagePasteIds[imageIndex]
isImage && uMsg.imagePasteIds
? (uMsg.imagePasteIds as number[])[imageIndex]
: undefined
if (isImage) imageIndex++
return {
...createUserMessage({
content: [_],
toolUseResult: message.toolUseResult,
mcpMeta: message.mcpMeta as { _meta?: Record<string, unknown>; structuredContent?: Record<string, unknown> },
isMeta: message.isMeta === true ? true : undefined,
isVisibleInTranscriptOnly: message.isVisibleInTranscriptOnly === true ? true : undefined,
isVirtual: (message.isVirtual as boolean | undefined) === true ? true : undefined,
timestamp: message.timestamp as string | undefined,
toolUseResult: uMsg.toolUseResult,
mcpMeta: uMsg.mcpMeta as { _meta?: Record<string, unknown>; structuredContent?: Record<string, unknown> },
isMeta: uMsg.isMeta === true ? true : undefined,
isVisibleInTranscriptOnly: uMsg.isVisibleInTranscriptOnly === true ? true : undefined,
isVirtual: (uMsg.isVirtual as boolean | undefined) === true ? true : undefined,
timestamp: uMsg.timestamp as string | undefined,
imagePasteIds: imageId !== undefined ? [imageId] : undefined,
origin: message.origin as MessageOrigin | undefined,
origin: uMsg.origin as MessageOrigin | undefined,
}),
uuid: isNewChain ? deriveUUID(message.uuid, index) : message.uuid,
uuid: isNewChain ? deriveUUID(uMsg.uuid, index) : uMsg.uuid,
} as NormalizedMessage
})
}
default:
return [message]
}
})
}
@@ -834,8 +841,8 @@ export function isToolUseRequestMessage(
return (
message.type === 'assistant' &&
// Note: stop_reason === 'tool_use' is unreliable -- it's not always set correctly
Array.isArray(message.message.content) &&
message.message.content.some(_ => _.type === 'tool_use')
Array.isArray(message.message?.content) &&
(message.message?.content as Array<{type: string}>).some(_ => _.type === 'tool_use')
)
}
@@ -848,8 +855,8 @@ export function isToolUseResultMessage(
): message is ToolUseResultMessage {
return (
message.type === 'user' &&
((Array.isArray(message.message.content) &&
message.message.content[0]?.type === 'tool_result') ||
((Array.isArray(message.message?.content) &&
(message.message?.content as Array<{type: string}>)[0]?.type === 'tool_result') ||
Boolean(message.toolUseResult))
)
}
@@ -1035,14 +1042,14 @@ function isHookAttachmentMessage(
): message is AttachmentMessage<HookAttachment> {
return (
message.type === 'attachment' &&
(message.attachment.type === 'hook_blocking_error' ||
message.attachment.type === 'hook_cancelled' ||
message.attachment.type === 'hook_error_during_execution' ||
message.attachment.type === 'hook_non_blocking_error' ||
message.attachment.type === 'hook_success' ||
message.attachment.type === 'hook_system_message' ||
message.attachment.type === 'hook_additional_context' ||
message.attachment.type === 'hook_stopped_continuation')
(message.attachment?.type === 'hook_blocking_error' ||
message.attachment?.type === 'hook_cancelled' ||
message.attachment?.type === 'hook_error_during_execution' ||
message.attachment?.type === 'hook_non_blocking_error' ||
message.attachment?.type === 'hook_success' ||
message.attachment?.type === 'hook_system_message' ||
message.attachment?.type === 'hook_additional_context' ||
message.attachment?.type === 'hook_stopped_continuation')
)
}
@@ -1105,11 +1112,11 @@ export function getToolResultIDs(normalizedMessages: NormalizedMessage[]): {
} {
return Object.fromEntries(
normalizedMessages.flatMap(_ =>
_.type === 'user' && Array.isArray(_.message.content) && _.message.content[0]?.type === 'tool_result'
_.type === 'user' && Array.isArray(_.message?.content) && (_.message?.content as Array<{type:string}>)[0]?.type === 'tool_result'
? [
[
(_.message.content[0] as ToolResultBlockParam).tool_use_id,
(_.message.content[0] as ToolResultBlockParam).is_error ?? false,
((_.message?.content as Array<{type:string}>)[0] as ToolResultBlockParam).tool_use_id,
((_.message?.content as Array<{type:string}>)[0] as ToolResultBlockParam).is_error ?? false,
],
]
: ([] as [string, boolean][]),
@@ -1129,8 +1136,8 @@ export function getSiblingToolUseIDs(
const unnormalizedMessage = messages.find(
(_): _ is AssistantMessage =>
_.type === 'assistant' &&
Array.isArray(_.message.content) &&
_.message.content.some(block => block.type === 'tool_use' && (block as ToolUseBlock).id === toolUseID),
Array.isArray(_.message?.content) &&
(_.message?.content as Array<{type:string; id?:string}>).some(block => block.type === 'tool_use' && block.id === toolUseID),
)
if (!unnormalizedMessage) {
return new Set()
@@ -1139,13 +1146,13 @@ export function getSiblingToolUseIDs(
const messageID = unnormalizedMessage.message.id
const siblingMessages = messages.filter(
(_): _ is AssistantMessage =>
_.type === 'assistant' && _.message.id === messageID,
_.type === 'assistant' && _.message?.id === messageID,
)
return new Set(
siblingMessages.flatMap(_ =>
Array.isArray(_.message.content)
? _.message.content.filter(_ => _.type === 'tool_use').map(_ => (_ as ToolUseBlock).id)
Array.isArray(_.message?.content)
? (_.message?.content as Array<{type:string; id?:string}>).filter(_ => _.type === 'tool_use').map(_ => _.id!)
: [],
),
)
@@ -1185,14 +1192,15 @@ export function buildMessageLookups(
const toolUseByToolUseID = new Map<string, ToolUseBlockParam>()
for (const msg of messages) {
if (msg.type === 'assistant') {
const id = msg.message.id
const aMsg = msg as AssistantMessage
const id = aMsg.message.id!
let toolUseIDs = toolUseIDsByMessageID.get(id)
if (!toolUseIDs) {
toolUseIDs = new Set()
toolUseIDsByMessageID.set(id, toolUseIDs)
}
if (Array.isArray(msg.message.content)) {
for (const content of msg.message.content) {
if (Array.isArray(aMsg.message.content)) {
for (const content of aMsg.message.content) {
if (typeof content !== 'string' && content.type === 'tool_use') {
const toolUseContent = content as ToolUseBlock
toolUseIDs.add(toolUseContent.id)
@@ -1247,8 +1255,8 @@ export function buildMessageLookups(
}
// Build tool result lookup and resolved/errored sets
if (msg.type === 'user' && Array.isArray(msg.message.content)) {
for (const content of msg.message.content) {
if (msg.type === 'user' && Array.isArray(msg.message?.content)) {
for (const content of (msg.message?.content ?? [])) {
if (typeof content !== 'string' && content.type === 'tool_result') {
const tr = content as ToolResultBlockParam
toolResultByToolUseID.set(tr.tool_use_id, msg)
@@ -1260,8 +1268,8 @@ export function buildMessageLookups(
}
}
if (msg.type === 'assistant' && Array.isArray(msg.message.content)) {
for (const content of msg.message.content) {
if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) {
for (const content of (msg.message?.content ?? [])) {
if (typeof content === 'string') continue
// Track all server-side *_tool_result blocks (advisor, web_search,
// code_execution, mcp, etc.) — any block with tool_use_id is a result.
@@ -1321,14 +1329,15 @@ export function buildMessageLookups(
// perpetually spinning.
const lastMsg = messages.at(-1)
const lastAssistantMsgId =
lastMsg?.type === 'assistant' ? lastMsg.message.id : undefined
lastMsg?.type === 'assistant' ? lastMsg.message?.id : undefined
for (const msg of normalizedMessages) {
if (msg.type !== 'assistant') continue
const aMsg = msg as AssistantMessage
// Skip blocks from the last original message if it's an assistant,
// since it may still be in progress.
if (msg.message.id === lastAssistantMsgId) continue
if (!Array.isArray(msg.message.content)) continue
for (const content of msg.message.content) {
if (aMsg.message.id === lastAssistantMsgId) continue
if (!Array.isArray(aMsg.message.content)) continue
for (const content of aMsg.message.content) {
if (
typeof content !== 'string' &&
((content.type as string) === 'server_tool_use' ||
@@ -1483,10 +1492,10 @@ export function getToolUseIDs(
.filter(
(_): _ is NormalizedAssistantMessage<BetaToolUseBlock> =>
_.type === 'assistant' &&
Array.isArray(_.message.content) &&
_.message.content[0]?.type === 'tool_use',
Array.isArray(_.message?.content) &&
(_.message?.content as Array<{type:string}>)[0]?.type === 'tool_use',
)
.map(_ => (_.message.content[0] as BetaToolUseBlock).id),
.map(_ => ((_.message?.content as Array<BetaToolUseBlock>)[0]).id),
)
}
@@ -1515,8 +1524,8 @@ export function reorderAttachmentsForAPI(messages: Message[]): Message[] {
const isStoppingPoint =
message.type === 'assistant' ||
(message.type === 'user' &&
Array.isArray(message.message.content) &&
message.message.content[0]?.type === 'tool_result')
Array.isArray(message.message?.content) &&
(message.message?.content as Array<{type:string}>)[0]?.type === 'tool_result')
if (isStoppingPoint && pendingAttachments.length > 0) {
// Hit a stopping point — attachments stop here (go after the stopping point).
@@ -1815,6 +1824,7 @@ function contentHasToolReference(
*/
function ensureSystemReminderWrap(msg: UserMessage): UserMessage {
const content = msg.message.content
if (!content) return msg
if (typeof content === 'string') {
if (content.startsWith('<system-reminder>')) return msg
return {
@@ -2397,8 +2407,8 @@ export function mergeUserMessagesAndToolResults(
a: UserMessage,
b: UserMessage,
): UserMessage {
const lastContent = normalizeUserTextContent(a.message.content)
const currentContent = normalizeUserTextContent(b.message.content)
const lastContent = normalizeUserTextContent(a.message.content as string | ContentBlockParam[])
const currentContent = normalizeUserTextContent(b.message.content as string | ContentBlockParam[])
return {
...a,
message: {
@@ -2430,14 +2440,14 @@ function isToolResultMessage(msg: Message): boolean {
if (msg.type !== 'user') {
return false
}
const content = msg.message.content
if (typeof content === 'string') return false
return content.some(block => block.type === 'tool_result')
const content = msg.message?.content
if (!content || typeof content === 'string') return false
return (content as Array<{type:string}>).some(block => block.type === 'tool_result')
}
export function mergeUserMessages(a: UserMessage, b: UserMessage): UserMessage {
const lastContent = normalizeUserTextContent(a.message.content)
const currentContent = normalizeUserTextContent(b.message.content)
const lastContent = normalizeUserTextContent(a.message.content as string | ContentBlockParam[])
const currentContent = normalizeUserTextContent(b.message.content as string | ContentBlockParam[])
if (feature('HISTORY_SNIP')) {
// A merged message is only meta if ALL merged messages are meta. If any
// operand is real user content, the result must not be flagged isMeta
@@ -2793,12 +2803,12 @@ export function getToolUseID(message: NormalizedMessage): string | null {
switch (message.type) {
case 'attachment':
if (isHookAttachmentMessage(message)) {
return message.attachment.toolUseID
return message.attachment.toolUseID ?? null
}
return null
case 'assistant': {
const aContent = Array.isArray(message.message.content) ? message.message.content : []
const firstBlock = aContent[0]
const aContent = Array.isArray(message.message?.content) ? message.message?.content : []
const firstBlock = aContent![0]
if (!firstBlock || typeof firstBlock === 'string' || firstBlock.type !== 'tool_use') {
return null
}
@@ -2808,8 +2818,8 @@ export function getToolUseID(message: NormalizedMessage): string | null {
if (message.sourceToolUseID) {
return message.sourceToolUseID as string
}
const uContent = Array.isArray(message.message.content) ? message.message.content : []
const firstUBlock = uContent[0]
const uContent = Array.isArray(message.message?.content) ? message.message?.content : []
const firstUBlock = uContent![0]
if (!firstUBlock || typeof firstUBlock === 'string' || firstUBlock.type !== 'tool_result') {
return null
}
@@ -2821,6 +2831,8 @@ export function getToolUseID(message: NormalizedMessage): string | null {
return (message.subtype as string) === 'informational'
? ((message.toolUseID as string) ?? null)
: null
default:
return null
}
}
@@ -2835,14 +2847,14 @@ export function filterUnresolvedToolUses(messages: Message[]): Message[] {
for (const msg of messages) {
if (msg.type !== 'user' && msg.type !== 'assistant') continue
const content = msg.message.content
const content = msg.message?.content
if (!Array.isArray(content)) continue
for (const block of content) {
for (const block of content as Array<{type:string; id?:string; tool_use_id?:string}>) {
if (block.type === 'tool_use') {
toolUseIds.add(block.id)
toolUseIds.add(block.id!)
}
if (block.type === 'tool_result') {
toolResultIds.add(block.tool_use_id)
toolResultIds.add(block.tool_use_id!)
}
}
}
@@ -2858,12 +2870,12 @@ export function filterUnresolvedToolUses(messages: Message[]): Message[] {
// Filter out assistant messages whose tool_use blocks are all unresolved
return messages.filter(msg => {
if (msg.type !== 'assistant') return true
const content = msg.message.content
const content = msg.message?.content
if (!Array.isArray(content)) return true
const toolUseBlockIds: string[] = []
for (const b of content) {
for (const b of content as Array<{type:string; id?:string}>) {
if (b.type === 'tool_use') {
toolUseBlockIds.push(b.id)
toolUseBlockIds.push(b.id!)
}
}
if (toolUseBlockIds.length === 0) return true
@@ -2878,11 +2890,11 @@ export function getAssistantMessageText(message: Message): string | null {
}
// For content blocks array, extract and concatenate text blocks
if (Array.isArray(message.message.content)) {
if (Array.isArray(message.message?.content)) {
return (
message.message.content
(message.message?.content as Array<{type:string; text?:string}>)
.filter(block => block.type === 'text')
.map(block => (block.type === 'text' ? block.text : ''))
.map(block => block.text ?? '')
.join('\n')
.trim() || null
)
@@ -2897,9 +2909,9 @@ export function getUserMessageText(
return null
}
const content = message.message.content
const content = message.message?.content
return getContentText(content)
return getContentText(content as string | ContentBlockParam[])
}
export function textForResubmit(
@@ -4462,7 +4474,7 @@ export function createStopHookSummaryMessage(
timestamp: new Date().toISOString(),
uuid: randomUUID(),
toolUseID,
hookLabel,
hookLabel: hookLabel ?? '',
totalDurationMs,
}
}
@@ -4720,8 +4732,8 @@ export function shouldShowUserMessage(
export function isThinkingMessage(message: Message): boolean {
if (message.type !== 'assistant') return false
if (!Array.isArray(message.message.content)) return false
return message.message.content.every(
if (!Array.isArray(message.message?.content)) return false
return (message.message?.content as Array<{type:string}>).every(
block => block.type === 'thinking' || block.type === 'redacted_thinking',
)
}
@@ -4738,8 +4750,8 @@ export function countToolCalls(
let count = 0
for (const msg of messages) {
if (!msg) continue
if (msg.type === 'assistant' && Array.isArray(msg.message.content)) {
const hasToolUse = msg.message.content.some(
if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) {
const hasToolUse = (msg.message?.content as Array<{type:string; name?:string}>).some(
(block): block is ToolUseBlock =>
block.type === 'tool_use' && block.name === toolName,
)
@@ -4767,8 +4779,8 @@ export function hasSuccessfulToolCall(
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i]
if (!msg) continue
if (msg.type === 'assistant' && Array.isArray(msg.message.content)) {
const toolUse = msg.message.content.find(
if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) {
const toolUse = (msg.message?.content as Array<{type:string; name?:string; id?:string}>).find(
(block): block is ToolUseBlock =>
block.type === 'tool_use' && block.name === toolName,
)
@@ -4785,8 +4797,8 @@ export function hasSuccessfulToolCall(
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i]
if (!msg) continue
if (msg.type === 'user' && Array.isArray(msg.message.content)) {
const toolResult = msg.message.content.find(
if (msg.type === 'user' && Array.isArray(msg.message?.content)) {
const toolResult = (msg.message?.content as Array<{type:string; tool_use_id?:string; is_error?:boolean}>).find(
(block): block is ToolResultBlockParam =>
block.type === 'tool_result' &&
block.tool_use_id === mostRecentToolUseId,
@@ -4925,8 +4937,7 @@ export function filterWhitespaceOnlyAssistantMessages(
return true
}
const content = message.message.content
// Keep messages with empty arrays (handled elsewhere) or that have real content
const content = message.message?.content
if (!Array.isArray(content) || content.length === 0) {
return true
}
@@ -5046,14 +5057,14 @@ export function filterOrphanedThinkingOnlyMessages(
for (const msg of messages) {
if (msg.type !== 'assistant') continue
const content = msg.message.content
const content = msg.message?.content
if (!Array.isArray(content)) continue
const hasNonThinking = content.some(
const hasNonThinking = (content as Array<{type:string}>).some(
block => block.type !== 'thinking' && block.type !== 'redacted_thinking',
)
if (hasNonThinking && msg.message.id) {
messageIdsWithNonThinkingContent.add(msg.message.id)
if (hasNonThinking && msg.message?.id) {
messageIdsWithNonThinkingContent.add(msg.message.id as string)
}
}
@@ -5063,13 +5074,13 @@ export function filterOrphanedThinkingOnlyMessages(
return true
}
const content = msg.message.content
const content = msg.message?.content
if (!Array.isArray(content) || content.length === 0) {
return true
}
// Check if ALL content blocks are thinking blocks
const allThinking = content.every(
const allThinking = (content as Array<{type:string}>).every(
block => block.type === 'thinking' || block.type === 'redacted_thinking',
)
@@ -5080,8 +5091,8 @@ export function filterOrphanedThinkingOnlyMessages(
// It's thinking-only. Keep it if there's another message with same id
// that has non-thinking content (they'll be merged later)
if (
msg.message.id &&
messageIdsWithNonThinkingContent.has(msg.message.id)
msg.message?.id &&
messageIdsWithNonThinkingContent.has(msg.message.id as string)
) {
return true
}
@@ -5091,7 +5102,7 @@ export function filterOrphanedThinkingOnlyMessages(
messageUUID:
msg.uuid as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
messageId: msg.message
.id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
?.id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
blockCount: content.length,
})
return false
@@ -5111,7 +5122,7 @@ export function stripSignatureBlocks(messages: Message[]): Message[] {
const result = messages.map(msg => {
if (msg.type !== 'assistant') return msg
const content = msg.message.content
const content = (msg as AssistantMessage).message.content
if (!Array.isArray(content)) return msg
const filtered = content.filter(block => {
@@ -5247,7 +5258,8 @@ export function ensureToolResultPairing(
// Collect server-side tool result IDs (*_tool_result blocks have tool_use_id).
const serverResultIds = new Set<string>()
for (const c of msg.message.content) {
const aMsg5 = msg as AssistantMessage
for (const c of aMsg5.message.content as (ContentBlockParam | ContentBlock)[]) {
if (typeof c !== 'string' && 'tool_use_id' in c && typeof (c as { tool_use_id: string }).tool_use_id === 'string') {
serverResultIds.add((c as { tool_use_id: string }).tool_use_id)
}
@@ -5266,7 +5278,7 @@ export function ensureToolResultPairing(
// has no matching *_tool_result and the API rejects with e.g. "advisor
// tool use without corresponding advisor_tool_result".
const seenToolUseIds = new Set<string>()
const assistantContent = Array.isArray(msg.message.content) ? msg.message.content : []
const assistantContent = Array.isArray(aMsg5.message.content) ? aMsg5.message.content : []
const finalContent = assistantContent.filter(block => {
if (typeof block === 'string') return true
if (block.type === 'tool_use') {
@@ -5288,7 +5300,7 @@ export function ensureToolResultPairing(
})
const assistantContentChanged =
finalContent.length !== msg.message.content.length
finalContent.length !== (aMsg5.message.content as (ContentBlockParam | ContentBlock)[]).length
// If stripping orphaned server tool uses empties the content array,
// insert a placeholder so the API doesn't reject empty assistant content.
@@ -5372,11 +5384,12 @@ export function ensureToolResultPairing(
if (nextMsg?.type === 'user') {
// Next message is already a user message - patch it
const nextUserMsg = nextMsg as UserMessage
let content: (ContentBlockParam | ContentBlock)[] = Array.isArray(
nextMsg.message.content,
nextUserMsg.message.content,
)
? nextMsg.message.content
: [{ type: 'text' as const, text: nextMsg.message.content }]
? nextUserMsg.message.content as (ContentBlockParam | ContentBlock)[]
: [{ type: 'text' as const, text: (nextUserMsg.message.content as string | undefined) ?? '' }]
// Strip orphaned tool_results and dedupe duplicate tool_result IDs
if (orphanedIds.length > 0 || hasDuplicateToolResults) {
@@ -5402,9 +5415,9 @@ export function ensureToolResultPairing(
// If content is now empty after stripping orphans, skip the user message
if (patchedContent.length > 0) {
const patchedNext: UserMessage = {
...nextMsg,
...nextUserMsg,
message: {
...nextMsg.message,
...nextUserMsg.message,
content: patchedContent,
},
}

View File

@@ -48,10 +48,9 @@ export function toInternalMessages(
uuid: message.uuid ?? randomUUID(),
timestamp: message.timestamp ?? new Date().toISOString(),
isMeta: message.isSynthetic,
} as Message,
} as unknown as Message,
]
case 'system':
// Handle compact boundary messages
// Handle compact boundary messages
if (message.subtype === 'compact_boundary') {
const compactMsg = message
return [
@@ -272,7 +271,7 @@ function normalizeAssistantMessageForSDK(
const normalizedContent = content.map((block): BetaContentBlock => {
if (block.type !== 'tool_use') {
return block
return block as unknown as BetaContentBlock
}
if (block.name === EXIT_PLAN_MODE_V2_TOOL_NAME) {

View File

@@ -171,13 +171,13 @@ export async function readNotebook(
const notebook = jsonParse(content) as NotebookContent
const language = notebook.metadata.language_info?.name ?? 'python'
if (cellId) {
const cell = notebook.cells.find(c => c.id === cellId)
const cell = notebook.cells.find((c: NotebookCell) => c.id === cellId)
if (!cell) {
throw new Error(`Cell with ID "${cellId}" not found in notebook`)
}
return [processCell(cell, notebook.cells.indexOf(cell), language, true)]
}
return notebook.cells.map((cell, index) =>
return notebook.cells.map((cell: NotebookCell, index: number) =>
processCell(cell, index, language, false),
)
}

View File

@@ -302,8 +302,8 @@ export type TranscriptEntry = {
export function buildTranscriptEntries(messages: Message[]): TranscriptEntry[] {
const transcript: TranscriptEntry[] = []
for (const msg of messages) {
if (msg.type === 'attachment' && msg.attachment.type === 'queued_command') {
const prompt = msg.attachment.prompt
if (msg.type === 'attachment' && msg.attachment!.type === 'queued_command') {
const prompt = msg.attachment!.prompt
let text: string | null = null
if (typeof prompt === 'string') {
text = prompt
@@ -324,7 +324,7 @@ export function buildTranscriptEntries(messages: Message[]): TranscriptEntry[] {
})
}
} else if (msg.type === 'user') {
const content = msg.message.content
const content = msg.message!.content
const textBlocks: TranscriptBlock[] = []
if (typeof content === 'string') {
textBlocks.push({ type: 'text', text: content })
@@ -340,7 +340,7 @@ export function buildTranscriptEntries(messages: Message[]): TranscriptEntry[] {
}
} else if (msg.type === 'assistant') {
const blocks: TranscriptBlock[] = []
for (const block of msg.message.content) {
for (const block of (msg.message!.content ?? [])) {
// Only include tool_use blocks — assistant text is model-authored
// and could be crafted to influence the classifier's decision.
if (typeof block !== 'string' && block.type === 'tool_use') {

View File

@@ -69,7 +69,7 @@ function convertPluginHooksToMatchers(
continue
}
for (const matcher of matchers) {
for (const matcher of (matchers ?? [])) {
if (matcher.hooks.length > 0) {
pluginMatchers[hookEvent].push({
matcher: matcher.matcher,
@@ -195,7 +195,7 @@ export async function pruneRemovedPluginHooks(): Promise<void> {
// clearRegisteredPluginHooks; we only need to re-register survivors.
const survivors: Partial<Record<HookEvent, PluginHookMatcher[]>> = {}
for (const [event, matchers] of Object.entries(current)) {
const kept = matchers.filter(
const kept = (matchers ?? []).filter(
(m): m is PluginHookMatcher =>
'pluginRoot' in m && enabledRoots.has(m.pluginRoot),
)

View File

@@ -259,7 +259,7 @@ export function resolvePluginLspEnvironment(
// Resolve args
if (resolved.args) {
resolved.args = resolved.args.map(arg => resolveValue(arg))
resolved.args = resolved.args.map((arg: string) => resolveValue(arg))
}
// Resolve environment variables and add CLAUDE_PLUGIN_ROOT / CLAUDE_PLUGIN_DATA

View File

@@ -81,6 +81,7 @@ import {
setPluginSettingsBase,
} from '../settings/settingsCache.js'
import type { HooksSettings } from '../settings/types.js'
import type { HookMatcher } from '../../schemas/hooks.js'
import { SettingsSchema } from '../settings/types.js'
import { jsonParse, jsonStringify } from '../slowOperations.js'
import { getAddDirEnabledPlugins } from './addDirPluginSettings.js'
@@ -1861,15 +1862,13 @@ function mergeHooksSettings(
const merged = { ...base }
for (const [event, matchers] of Object.entries(additional)) {
for (const [event, matchers] of Object.entries(additional) as [string, HookMatcher[]][]) {
if (!merged[event as keyof HooksSettings]) {
merged[event as keyof HooksSettings] = matchers
} else {
// Merge matchers for this event
merged[event as keyof HooksSettings] = [
...(merged[event as keyof HooksSettings] || []),
...matchers,
]
const existing = ((merged[event as keyof HooksSettings] as unknown) ?? []) as HookMatcher[]
merged[event as keyof HooksSettings] = [...existing, ...matchers]
}
}

View File

@@ -241,17 +241,17 @@ export async function processUserInput({
// TODO: Clean this up
if (hookResult.message) {
switch (hookResult.message.attachment.type) {
switch (hookResult.message.attachment!.type) {
case 'hook_success':
if (!hookResult.message.attachment.content) {
if (!hookResult.message.attachment!.content) {
// Skip if there is no content
break
}
result.messages.push({
...hookResult.message,
attachment: {
...hookResult.message.attachment,
content: applyTruncation(hookResult.message.attachment.content as string),
...hookResult.message.attachment!,
content: applyTruncation(hookResult.message.attachment!.content as string),
},
} as AttachmentMessage)
break

View File

@@ -135,7 +135,7 @@ export async function buildSideQuestionFallbackParams({
// as btw.tsx. The SDK can fire side_question mid-turn.
const last = messages.at(-1)
const forkContextMessages =
last?.type === 'assistant' && last.message.stop_reason === null
last?.type === 'assistant' && last.message!.stop_reason === null
? messages.slice(0, -1)
: messages

View File

@@ -68,7 +68,8 @@ export function isResultSuccessful(
if (!message) return false
if (message.type === 'assistant') {
const lastContent = last(message.message.content)
const content = message.message!.content
const lastContent = Array.isArray(content) ? content[content.length - 1] : undefined
return (
lastContent?.type === 'text' ||
lastContent?.type === 'thinking' ||
@@ -78,7 +79,7 @@ export function isResultSuccessful(
if (message.type === 'user') {
// Check if all content blocks are tool_result type
const content = message.message.content
const content = message.message!.content
if (
Array.isArray(content) &&
content.length > 0 &&
@@ -323,8 +324,8 @@ export async function* handleOrphanedPermission(
const alreadyPresent = mutableMessages.some(
m =>
m.type === 'assistant' &&
Array.isArray(m.message.content) &&
m.message.content.some(
Array.isArray(m.message!.content) &&
m.message!.content.some(
b => b.type === 'tool_use' && 'id' in b && b.id === toolUseID,
),
)
@@ -385,9 +386,9 @@ export function extractReadFilesFromMessages(
for (const message of messages) {
if (
message.type === 'assistant' &&
Array.isArray(message.message.content)
Array.isArray(message.message!.content)
) {
for (const content of message.message.content) {
for (const content of message.message!.content) {
if (
content.type === 'tool_use' &&
content.name === FILE_READ_TOOL_NAME
@@ -442,8 +443,8 @@ export function extractReadFilesFromMessages(
// Second pass: find corresponding tool results and extract content
for (const message of messages) {
if (message.type === 'user' && Array.isArray(message.message.content)) {
for (const content of message.message.content) {
if (message.type === 'user' && Array.isArray(message.message!.content)) {
for (const content of message.message!.content) {
if (content.type === 'tool_result' && content.tool_use_id) {
// Handle Read tool results
const readFilePath = fileReadToolUseIds.get(content.tool_use_id)
@@ -537,9 +538,9 @@ export function extractBashToolsFromMessages(messages: Message[]): Set<string> {
for (const message of messages) {
if (
message.type === 'assistant' &&
Array.isArray(message.message.content)
Array.isArray(message.message!.content)
) {
for (const content of message.message.content) {
for (const content of message.message!.content) {
if (content.type === 'tool_use' && content.name === BASH_TOOL_NAME) {
const { input } = content
if (

View File

@@ -78,7 +78,7 @@ 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 toolUse = (msg.message!.content as any[]).find(
block => block.type === 'tool_use' && block.name === TODO_WRITE_TOOL_NAME,
)
if (!toolUse || toolUse.type !== 'tool_use') continue

View File

@@ -1927,9 +1927,9 @@ function applyPreservedSegmentRelinks(
messages.set(uuid, {
...msg,
message: {
...msg.message,
...msg.message!,
usage: {
...msg.message.usage,
...msg.message!.usage,
input_tokens: 0,
output_tokens: 0,
cache_creation_input_tokens: 0,
@@ -2131,7 +2131,7 @@ function recoverOrphanedParallelToolResults(
// already in chain order, so later iterations overwrite → last wins.
const anchorByMsgId = new Map<string, ChainAssistant>()
for (const a of chainAssistants) {
if (a.message.id) anchorByMsgId.set(a.message.id, a)
if (a.message!.id) anchorByMsgId.set(a.message!.id, a)
}
// O(n) precompute: sibling groups and TR index.
@@ -2140,15 +2140,15 @@ function recoverOrphanedParallelToolResults(
const siblingsByMsgId = new Map<string, TranscriptMessage[]>()
const toolResultsByAsst = new Map<UUID, TranscriptMessage[]>()
for (const m of messages.values()) {
if (m.type === 'assistant' && m.message.id) {
const group = siblingsByMsgId.get(m.message.id)
if (m.type === 'assistant' && m.message!.id) {
const group = siblingsByMsgId.get(m.message!.id)
if (group) group.push(m)
else siblingsByMsgId.set(m.message.id, [m])
else siblingsByMsgId.set(m.message!.id, [m])
} else if (
m.type === 'user' &&
m.parentUuid &&
Array.isArray(m.message.content) &&
m.message.content.some(b => b.type === 'tool_result')
Array.isArray(m.message!.content) &&
(m.message!.content as Array<{type: string}>).some(b => b.type === 'tool_result')
) {
const group = toolResultsByAsst.get(m.parentUuid)
if (group) group.push(m)
@@ -2164,7 +2164,7 @@ function recoverOrphanedParallelToolResults(
const inserts = new Map<UUID, TranscriptMessage[]>()
let recoveredCount = 0
for (const asst of chainAssistants) {
const msgId = asst.message.id
const msgId = asst.message!.id
if (!msgId || processedGroups.has(msgId)) continue
processedGroups.add(msgId)
@@ -4357,7 +4357,7 @@ export function isLoggableMessage(m: Message): boolean {
// user-configured hook output that is useful for session context on resume.
if (m.type === 'attachment' && getUserType() !== 'ant') {
if (
m.attachment.type === 'hook_additional_context' &&
m.attachment!.type === 'hook_additional_context' &&
isEnvTruthy(process.env.CLAUDE_CODE_SAVE_HOOK_ADDITIONAL_CONTEXT)
) {
return true
@@ -4370,8 +4370,8 @@ export function isLoggableMessage(m: Message): boolean {
function collectReplIds(messages: readonly Message[]): Set<string> {
const ids = new Set<string>()
for (const m of messages) {
if (m.type === 'assistant' && Array.isArray(m.message.content)) {
for (const b of m.message.content) {
if (m.type === 'assistant' && Array.isArray(m.message!.content)) {
for (const b of m.message!.content as Array<{type: string; name: string; id: string}>) {
if (b.type === 'tool_use' && b.name === REPL_TOOL_NAME) {
ids.add(b.id)
}
@@ -4488,9 +4488,9 @@ export async function findUnresolvedToolUse(
// Find the tool use but make sure there's not also a result
for (const message of messages.values()) {
if (message.type === 'assistant') {
const content = message.message.content
const content = message.message!.content
if (Array.isArray(content)) {
for (const block of content) {
for (const block of content as Array<{type: string; id: string}>) {
if (block.type === 'tool_use' && block.id === toolUseId) {
toolUseMessage = message
break
@@ -4498,9 +4498,9 @@ export async function findUnresolvedToolUse(
}
}
} else if (message.type === 'user') {
const content = message.message.content
const content = message.message!.content
if (Array.isArray(content)) {
for (const block of content) {
for (const block of content as Array<{type: string; tool_use_id: string}>) {
if (
block.type === 'tool_result' &&
block.tool_use_id === toolUseId
@@ -4513,7 +4513,7 @@ export async function findUnresolvedToolUse(
}
}
return toolUseMessage
return toolUseMessage as AssistantMessage | null
} catch {
return null
}

View File

@@ -36,7 +36,7 @@ export function extractConversationText(messages: Message[]): string {
if (msg.type !== 'user' && msg.type !== 'assistant') continue
if ('isMeta' in msg && msg.isMeta) continue
if ('origin' in msg && (msg as any).origin && (msg as any).origin.kind !== 'human') continue
const content = msg.message.content
const content = msg.message!.content
if (typeof content === 'string') {
parts.push(content)
} else if (Array.isArray(content)) {

View File

@@ -125,7 +125,7 @@ ${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) {

View File

@@ -110,7 +110,7 @@ function accumulateToolUses(
message: SDKAssistantMessage,
counts: ToolCounts,
): void {
const content = message.message.content
const content = message.message!.content
if (!Array.isArray(content)) {
return
}

View File

@@ -1235,7 +1235,7 @@ export async function runInProcessTeammate(
// Track in-progress tool use IDs for animation in transcript view
let inProgressToolUseIDs = task.inProgressToolUseIDs
if (message.type === 'assistant') {
for (const block of (Array.isArray(message.message.content) ? message.message.content : [])) {
for (const block of (Array.isArray(message.message!.content) ? message.message!.content : [])) {
if (typeof block !== 'string' && block.type === 'tool_use') {
inProgressToolUseIDs = new Set([
...(inProgressToolUseIDs ?? []),
@@ -1244,7 +1244,7 @@ export async function runInProcessTeammate(
}
}
} else if (message.type === 'user') {
const content = message.message.content
const content = message.message!.content
if (Array.isArray(content)) {
for (const block of content) {
if (

View File

@@ -1152,7 +1152,7 @@ export function getLastPeerDmSummary(messages: Message[]): string | undefined {
if (!msg) continue
// Stop at wake-up boundary: a user prompt (string content), not tool results (array content)
if (msg.type === 'user' && typeof msg.message.content === 'string') {
if (msg.type === 'user' && typeof msg.message!.content === 'string') {
break
}

View File

@@ -184,7 +184,7 @@ async function generateTitleAndBranch(
})
// Extract text from the response
const firstBlock = response.message.content[0] as { type?: string; text?: string } | undefined
const firstBlock = response.message!.content?.[0] as { type?: string; text?: string } | undefined
if (firstBlock?.type !== 'text') {
return { title: fallbackTitle, branchName: fallbackBranch }
}

View File

@@ -30,10 +30,10 @@ export function getTokenUsage(message: Message): Usage | undefined {
function getAssistantMessageId(message: Message): string | undefined {
if (
message?.type === 'assistant' &&
'id' in message.message &&
message.message.model !== SYNTHETIC_MODEL
'id' in message.message! &&
message.message!.model !== SYNTHETIC_MODEL
) {
return message.message.id
return message.message!.id
}
return undefined
}

View File

@@ -537,7 +537,7 @@ function buildToolNameMap(messages: Message[]): Map<string, string> {
const map = new Map<string, string>()
for (const message of messages) {
if (message.type !== 'assistant') continue
const content = message.message.content
const content = message.message!.content
if (!Array.isArray(content)) continue
for (const block of content) {
if (block.type === 'tool_use') {
@@ -555,10 +555,10 @@ function buildToolNameMap(messages: Message[]): Map<string, string> {
* Returns [] for messages with no eligible blocks.
*/
function collectCandidatesFromMessage(message: Message): ToolResultCandidate[] {
if (message.type !== 'user' || !Array.isArray(message.message.content)) {
if (message.type !== 'user' || !Array.isArray(message.message!.content)) {
return []
}
return message.message.content.flatMap(block => {
return message.message!.content.flatMap(block => {
if (block.type !== 'tool_result' || !block.content) return []
if (isContentAlreadyCompacted(block.content)) return []
if (hasImageBlock(block.content)) return []
@@ -625,9 +625,9 @@ function collectCandidatesByMessage(
if (message.type === 'user') {
current.push(...collectCandidatesFromMessage(message))
} else if (message.type === 'assistant') {
if (!seenAsstIds.has(message.message.id)) {
if (!seenAsstIds.has(message.message!.id ?? '')) {
flush()
seenAsstIds.add(message.message.id)
seenAsstIds.add(message.message!.id ?? '')
}
}
// progress / attachment / system are filtered or merged by
@@ -701,10 +701,10 @@ function replaceToolResultContents(
replacementMap: Map<string, string>,
): Message[] {
return messages.map(message => {
if (message.type !== 'user' || !Array.isArray(message.message.content)) {
if (message.type !== 'user' || !Array.isArray(message.message!.content)) {
return message
}
const content = message.message.content
const content = message.message!.content
const needsReplace = content.some(
b => b.type === 'tool_result' && replacementMap.has(b.tool_use_id),
)

View File

@@ -655,11 +655,11 @@ export function getDeferredToolsDelta(
for (const msg of messages) {
if (msg.type !== 'attachment') continue
attachmentCount++
attachmentTypesSeen.add(msg.attachment.type)
if (msg.attachment.type !== 'deferred_tools_delta') continue
attachmentTypesSeen.add(msg.attachment!.type)
if (msg.attachment!.type !== 'deferred_tools_delta') continue
dtdCount++
for (const n of (msg.attachment as any).addedNames) announced.add(n)
for (const n of (msg.attachment as any).removedNames) announced.delete(n)
for (const n of msg.attachment!.addedNames) announced.add(n)
for (const n of msg.attachment!.removedNames) announced.delete(n)
}
const deferred: Tool[] = tools.filter(isDeferredTool)

View File

@@ -33,12 +33,12 @@ function computeSearchText(msg: RenderableMessage): string {
let raw = ''
switch (msg.type) {
case 'user': {
const c = msg.message.content
const c = msg.message!.content
if (typeof c === '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') {
@@ -83,8 +83,8 @@ function computeSearchText(msg: RenderableMessage): string {
// relevant_memories renders full m.content in transcript mode
// (AttachmentMessage.tsx <Ansi>{m.content}</Ansi>). Visible but
// unsearchable without this — [ dump finds it, / doesn't.
if (msg.attachment.type === 'relevant_memories') {
raw = msg.attachment.memories.map(m => m.content).join('\n')
if (msg.attachment!.type === 'relevant_memories') {
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