export interface AttachedFileMeta { fileName: string; mimeType: string; fileSize: number; preview: string | null; filePath?: string; source?: 'user-upload' | 'tool-result' | 'message-ref'; } export interface ContentBlockSource { type: string; media_type?: string; data?: string; url?: string; } export interface ContentBlock { type: 'text' | 'image' | 'thinking' | 'tool_use' | 'tool_result' | 'toolCall' | 'toolResult'; text?: string; thinking?: string; source?: ContentBlockSource; data?: string; mimeType?: string; id?: string; name?: string; input?: unknown; arguments?: unknown; content?: string | ContentBlock[]; } export type RawMessageRole = 'user' | 'assistant' | 'system' | 'toolresult' | 'tool_result'; export interface RawMessage { role: RawMessageRole; content: string | ContentBlock[]; timestamp?: number; id?: string; toolCallId?: string; toolName?: string; details?: unknown; isError?: boolean; question?: string[]; toolCall?: Record | null; _attachedFiles?: AttachedFileMeta[]; } export interface ToolStatus { id?: string; toolCallId?: string; name: string; status: 'running' | 'completed' | 'error'; durationMs?: number; summary?: string; updatedAt: number; } export interface ChatSession { key: string; label?: string; displayName?: string; thinkingLevel?: string; model?: string; updatedAt?: number; } export function extractText(message?: RawMessage | null): string { if (!message) return ''; if (typeof message.content === 'string') { return message.content; } return message.content .filter((block) => block.type === 'text' && typeof block.text === 'string') .map((block) => block.text ?? '') .join('\n'); } export function extractThinking(message?: RawMessage | null): string | null { if (!message || !Array.isArray(message.content)) return null; const thinkingBlock = message.content.find((block) => block.type === 'thinking'); return thinkingBlock?.thinking ?? null; } export function extractImages(message?: RawMessage | null): Array<{ url?: string; data?: string; mimeType: string }> { if (!message || !Array.isArray(message.content)) return []; const images: Array<{ url?: string; data?: string; mimeType: string }> = []; for (const block of message.content) { if (block.type === 'image') { if (block.source?.type === 'base64' && block.source.data) { images.push({ data: block.source.data, mimeType: block.source.media_type || 'image/jpeg', }); } else if (block.source?.type === 'url' && block.source.url) { images.push({ url: block.source.url, mimeType: block.source.media_type || 'image/jpeg', }); } else if (block.data) { images.push({ data: block.data, mimeType: block.mimeType || 'image/jpeg', }); } } if ((block.type === 'tool_result' || block.type === 'toolResult') && Array.isArray(block.content)) { images.push(...extractImages({ role: 'toolresult', content: block.content })); } } return images; } export function extractToolUse(message?: RawMessage | null): Array<{ id?: string; name: string; input?: unknown }> { if (!message || !Array.isArray(message.content)) return []; return message.content .filter((block) => block.type === 'tool_use' || block.type === 'toolCall') .map((block) => ({ id: block.id, name: block.name || block.id || 'tool', input: block.input ?? block.arguments, })); } export function formatTimestamp(ts?: number): string { if (!ts) return ''; const ms = ts < 1e12 ? ts * 1000 : ts; return new Date(ms).toLocaleString(); } export function isToolResultRole(role?: string): boolean { if (!role) return false; const normalized = role.toLowerCase(); return normalized === 'toolresult' || normalized === 'tool_result'; } export function isToolOnlyMessage(message?: RawMessage): boolean { if (!message) return false; if (isToolResultRole(message.role)) return true; if (!Array.isArray(message.content)) return false; const hasToolBlock = message.content.some((block) => ['tool_use', 'tool_result', 'toolCall', 'toolResult'].includes(block.type), ); const hasTextBlock = message.content.some((block) => block.type === 'text' && block.text?.trim()); const hasImageBlock = message.content.some((block) => block.type === 'image'); return hasToolBlock && !hasTextBlock && !hasImageBlock; } export function isInternalMessage(message: { role?: string; content?: unknown }): boolean { if (message.role === 'system') return true; if (message.role === 'assistant') { const text = typeof message.content === 'string' ? message.content : extractText(message as RawMessage); if (/^(HEARTBEAT_OK|NO_REPLY)\s*$/.test(text)) { return true; } } return false; }