diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 63a0d3b4..0ee4fa5d 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -1180,7 +1180,10 @@ function App() { const contentParts = msg.content as Array<{ type: string text?: string - attachment?: NonNullable[number] + path?: string + filename?: string + mimeType?: string + size?: number toolCallId?: string toolName?: string arguments?: ToolUIPart['input'] @@ -1191,13 +1194,16 @@ function App() { .map((part) => part.text || '') .join('') - const attachmentParts = contentParts.filter((part) => part.type === 'attachment' && part.attachment) + const attachmentParts = contentParts.filter((part) => part.type === 'attachment' && part.path) if (attachmentParts.length > 0) { - msgAttachments = attachmentParts - .map((part) => part.attachment) - .filter((attachment): attachment is NonNullable[number] => Boolean(attachment)) + msgAttachments = attachmentParts.map((part) => ({ + path: part.path!, + filename: part.filename || part.path!.split('/').pop() || part.path!, + mimeType: part.mimeType || 'application/octet-stream', + size: part.size, + })) } - + // Also extract tool-call parts from assistant messages if (msg.role === 'assistant') { for (const part of contentParts) { @@ -1654,10 +1660,9 @@ function App() { const userMessageId = `user-${Date.now()}` const displayAttachments: ChatMessage['attachments'] = hasAttachments ? stagedAttachments.map((attachment) => ({ - type: attachment.isImage ? 'image' : 'file', path: attachment.path, filename: attachment.filename, - mediaType: attachment.mediaType, + mimeType: attachment.mimeType, size: attachment.size, thumbnailUrl: attachment.thumbnailUrl, })) @@ -1697,13 +1702,10 @@ function App() { | { type: 'text'; text: string } | { type: 'attachment' - attachment: { - type: 'file' | 'image' - path: string - filename: string - mediaType: string - size?: number - } + path: string + filename: string + mimeType: string + size?: number } const contentParts: ContentPart[] = [] @@ -1712,12 +1714,9 @@ function App() { for (const mention of mentions) { contentParts.push({ type: 'attachment', - attachment: { - type: 'file', - path: mention.path, - filename: mention.displayName || mention.path.split('/').pop() || mention.path, - mediaType: 'text/markdown', - }, + path: mention.path, + filename: mention.displayName || mention.path.split('/').pop() || mention.path, + mimeType: 'text/markdown', }) } } @@ -1725,13 +1724,10 @@ function App() { for (const attachment of stagedAttachments) { contentParts.push({ type: 'attachment', - attachment: { - type: attachment.isImage ? 'image' : 'file', - path: attachment.path, - filename: attachment.filename, - mediaType: attachment.mediaType, - size: attachment.size, - }, + path: attachment.path, + filename: attachment.filename, + mimeType: attachment.mimeType, + size: attachment.size, }) } diff --git a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx index 7ab65a45..d3554c00 100644 --- a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx +++ b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx @@ -37,7 +37,7 @@ export type StagedAttachment = { id: string path: string filename: string - mediaType: string + mimeType: string isImage: boolean size: number thumbnailUrl?: string @@ -130,7 +130,7 @@ function ChatInputInner({ id: `att-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, path: filePath, filename: getFileDisplayName(filePath), - mediaType: mime, + mimeType: mime, isImage: image, size: result.size, thumbnailUrl: image ? `data:${mime};base64,${result.data}` : undefined, diff --git a/apps/x/apps/renderer/src/components/chat-message-attachments.tsx b/apps/x/apps/renderer/src/components/chat-message-attachments.tsx index 40747045..298e5f03 100644 --- a/apps/x/apps/renderer/src/components/chat-message-attachments.tsx +++ b/apps/x/apps/renderer/src/components/chat-message-attachments.tsx @@ -54,13 +54,13 @@ function ImageAttachmentPreview({ attachment }: { attachment: MessageAttachment () => async () => { try { const result = await window.ipc.invoke('shell:readFileBase64', { path: attachment.path }) - const mimeType = result.mimeType || attachment.mediaType || 'image/*' + const mimeType = result.mimeType || attachment.mimeType || 'image/*' setSrc(`data:${mimeType};base64,${result.data}`) } catch { // Keep current src; fallback rendering (broken image icon) is better than crashing. } }, - [attachment.mediaType, attachment.path] + [attachment.mimeType, attachment.path] ) useEffect(() => { @@ -91,8 +91,8 @@ interface ChatMessageAttachmentsProps { export function ChatMessageAttachments({ attachments, className }: ChatMessageAttachmentsProps) { if (attachments.length === 0) return null - const imageAttachments = attachments.filter((attachment) => isImageMime(attachment.mediaType)) - const fileAttachments = attachments.filter((attachment) => !isImageMime(attachment.mediaType)) + const imageAttachments = attachments.filter((attachment) => isImageMime(attachment.mimeType)) + const fileAttachments = attachments.filter((attachment) => !isImageMime(attachment.mimeType)) return (
diff --git a/apps/x/apps/renderer/src/lib/attachment-presentation.ts b/apps/x/apps/renderer/src/lib/attachment-presentation.ts index 7948cf1a..7ddedd30 100644 --- a/apps/x/apps/renderer/src/lib/attachment-presentation.ts +++ b/apps/x/apps/renderer/src/lib/attachment-presentation.ts @@ -3,7 +3,7 @@ import { getExtension } from '@/lib/file-utils' export type AttachmentLike = { filename?: string path: string - mediaType: string + mimeType: string } export type AttachmentIconKind = @@ -39,7 +39,7 @@ export function getAttachmentTypeLabel(attachment: AttachmentLike): string { const ext = getExtension(getAttachmentDisplayName(attachment)) if (ext) return ext.toUpperCase() - const mediaType = attachment.mediaType.toLowerCase() + const mediaType = attachment.mimeType.toLowerCase() if (mediaType.startsWith('audio/')) return 'AUDIO' if (mediaType.startsWith('video/')) return 'VIDEO' if (mediaType.startsWith('text/')) return 'TEXT' @@ -52,7 +52,7 @@ export function getAttachmentTypeLabel(attachment: AttachmentLike): string { } export function getAttachmentIconKind(attachment: AttachmentLike): AttachmentIconKind { - const mediaType = attachment.mediaType.toLowerCase() + const mediaType = attachment.mimeType.toLowerCase() const ext = getExtension(attachment.filename || attachment.path) if (mediaType.startsWith('audio/')) return 'audio' diff --git a/apps/x/apps/renderer/src/lib/chat-conversation.ts b/apps/x/apps/renderer/src/lib/chat-conversation.ts index 5d52fcd7..830e250b 100644 --- a/apps/x/apps/renderer/src/lib/chat-conversation.ts +++ b/apps/x/apps/renderer/src/lib/chat-conversation.ts @@ -3,10 +3,9 @@ import z from 'zod' import { AskHumanRequestEvent, ToolPermissionRequestEvent } from '@x/shared/src/runs.js' export interface MessageAttachment { - type: 'file' | 'image' path: string filename: string - mediaType: string + mimeType: string size?: number thumbnailUrl?: string } diff --git a/apps/x/packages/core/src/agents/runtime.ts b/apps/x/packages/core/src/agents/runtime.ts index bfbff41f..e1924523 100644 --- a/apps/x/packages/core/src/agents/runtime.ts +++ b/apps/x/packages/core/src/agents/runtime.ts @@ -416,23 +416,19 @@ export function convertFromMessages(messages: z.infer[]): ModelM } else { // New content parts array — collapse to text for LLM const textSegments: string[] = []; + const attachmentLines: string[] = []; - // Collect attachments into a header block - const attachmentParts = msg.content.filter((p: { type: string }) => p.type === "attachment"); - if (attachmentParts.length > 0) { - textSegments.push("User has attached the following files:"); - for (const part of attachmentParts) { - const att = (part as { type: string; attachment: { filename: string; mediaType: string; size?: number; path: string } }).attachment; - const sizeStr = att.size ? `, ${formatBytes(att.size)}` : ''; - textSegments.push(`- ${att.filename} (${att.mediaType}${sizeStr}) at ${att.path}`); + for (const part of msg.content) { + if (part.type === "attachment") { + const sizeStr = part.size ? `, ${formatBytes(part.size)}` : ''; + attachmentLines.push(`- ${part.filename} (${part.mimeType}${sizeStr}) at ${part.path}`); + } else { + textSegments.push(part.text); } - textSegments.push(""); // blank line separator } - // Collect text parts - const textParts = msg.content.filter((p: { type: string }) => p.type === "text"); - for (const part of textParts) { - textSegments.push((part as { type: string; text: string }).text); + if (attachmentLines.length > 0) { + textSegments.unshift("User has attached the following files:", ...attachmentLines, ""); } result.push({ diff --git a/apps/x/packages/core/src/runs/repo.ts b/apps/x/packages/core/src/runs/repo.ts index 8438555d..5d563f1f 100644 --- a/apps/x/packages/core/src/runs/repo.ts +++ b/apps/x/packages/core/src/runs/repo.ts @@ -49,10 +49,10 @@ export class FSRunsRepo implements IRunsRepo { let textContent: string | undefined; if (typeof content === 'string') { textContent = content; - } else if (Array.isArray(content)) { + } else { textContent = content - .filter((p: { type: string }) => p.type === 'text') - .map((p: { type: string; text?: string }) => p.text || '') + .filter(p => p.type === 'text') + .map(p => p.text) .join(''); } if (textContent && textContent.trim()) { @@ -101,10 +101,10 @@ export class FSRunsRepo implements IRunsRepo { let textContent: string | undefined; if (typeof content === 'string') { textContent = content; - } else if (Array.isArray(content)) { + } else { textContent = content - .filter((p: { type: string }) => p.type === 'text') - .map((p: { type: string; text?: string }) => p.text || '') + .filter(p => p.type === 'text') + .map(p => p.text) .join(''); } if (textContent && textContent.trim()) { @@ -257,13 +257,5 @@ export class FSRunsRepo implements IRunsRepo { async delete(id: string): Promise { const filePath = path.join(WorkDir, 'runs', `${id}.jsonl`); await fsp.unlink(filePath); - // Clean up attachment sidecar directory if it exists - const attachmentsDir = path.join(WorkDir, 'runs', 'attachments', id); - try { - await fsp.rm(attachmentsDir, { recursive: true }); - } catch (err: unknown) { - const e = err as { code?: string }; - if (e.code !== 'ENOENT') throw err; - } } } \ No newline at end of file diff --git a/apps/x/packages/shared/src/message.ts b/apps/x/packages/shared/src/message.ts index a8765a0f..be761853 100644 --- a/apps/x/packages/shared/src/message.ts +++ b/apps/x/packages/shared/src/message.ts @@ -28,15 +28,6 @@ export const AssistantContentPart = z.union([ ToolCallPart, ]); -// Metadata about an attached file or image -export const Attachment = z.object({ - type: z.enum(["file", "image"]), // extensible — could add "url", "audio" later - path: z.string(), // absolute file path - filename: z.string(), // display name ("photo.png") - mediaType: z.string(), // MIME type ("image/png", "text/plain") - size: z.number().optional(), // bytes -}); - // A piece of user-typed text within a content array export const UserTextPart = z.object({ type: z.literal("text"), @@ -46,7 +37,10 @@ export const UserTextPart = z.object({ // An attachment within a content array export const UserAttachmentPart = z.object({ type: z.literal("attachment"), - attachment: Attachment, + path: z.string(), // absolute file path + filename: z.string(), // display name ("photo.png") + mimeType: z.string(), // MIME type ("image/png", "text/plain") + size: z.number().optional(), // bytes }); // Any single part of a user message (text or attachment)