diff --git a/app/src/lib/attachments.test.ts b/app/src/lib/attachments.test.ts index 4323ac9a1..4b67e1378 100644 --- a/app/src/lib/attachments.test.ts +++ b/app/src/lib/attachments.test.ts @@ -256,6 +256,41 @@ describe('parseMessageImages', () => { expect(result.text).toBe(''); expect(result.dataUris).toHaveLength(1); }); + + it('strips FILE markers without including them in dataUris', () => { + const result = parseMessageImages('read this [FILE:data:application/pdf;base64,xyz]'); + expect(result.text).toBe('read this'); + expect(result.dataUris).toEqual([]); + }); + + it('handles mixed IMAGE and FILE markers', () => { + const result = parseMessageImages( + 'check this [IMAGE:data:image/png;base64,a] and [FILE:data:application/pdf;base64,b] thanks' + ); + expect(result.text).toBe('check this and thanks'); + expect(result.dataUris).toEqual(['data:image/png;base64,a']); + }); + + it('strips multiple FILE markers', () => { + const result = parseMessageImages( + '[FILE:data:application/pdf;base64,a] [FILE:data:text/plain;base64,b]' + ); + expect(result.text).toBe(''); + expect(result.dataUris).toEqual([]); + }); + + it('preserves paragraph breaks (double newlines) in user text', () => { + const result = parseMessageImages('first paragraph\n\nsecond paragraph'); + expect(result.text).toBe('first paragraph\n\nsecond paragraph'); + }); + + it('preserves newlines when stripping a marker', () => { + const result = parseMessageImages( + 'first paragraph\n\n[FILE:data:application/pdf;base64,x]\n\nsecond paragraph' + ); + expect(result.text).toBe('first paragraph\n\n\n\nsecond paragraph'); + expect(result.text).toContain('\n\n'); + }); }); describe('formatFileSize', () => { diff --git a/app/src/lib/attachments.ts b/app/src/lib/attachments.ts index 4b6a92382..22395bd3a 100644 --- a/app/src/lib/attachments.ts +++ b/app/src/lib/attachments.ts @@ -247,8 +247,9 @@ export function buildMessageWithAttachments(text: string, attachments: Attachmen } /** - * Parse `[IMAGE:]` markers out of a stored message string. - * Returns the clean text (markers removed) and the list of data URIs found. + * Parse `[IMAGE:]` and `[FILE:]` markers out of a stored message string. + * Returns the clean text (markers removed) and the list of image data URIs found. + * File markers are stripped from text but not returned (file data lives in extraMetadata). */ export function parseMessageImages(content: string): { text: string; dataUris: string[] } { const dataUris: string[] = []; @@ -257,6 +258,11 @@ export function parseMessageImages(content: string): { text: string; dataUris: s dataUris.push(uri); return ''; }) + .replace(/\[FILE:([^\]]+)\]/g, '') // Strip file markers + // Collapse only runs of plain spaces (not \s) left behind by marker + // removal — using \s here would also eat intentional newlines/paragraph + // breaks in the user's own text. + .replace(/ {2,}/g, ' ') .trim(); return { text, dataUris }; } diff --git a/app/src/pages/Conversations.tsx b/app/src/pages/Conversations.tsx index e47860af1..3377868d4 100644 --- a/app/src/pages/Conversations.tsx +++ b/app/src/pages/Conversations.tsx @@ -1835,6 +1835,13 @@ const Conversations = ({ )} {visibleMessages.map(msg => { const isAgentTextMode = msg.sender === 'agent' && agentMessageViewMode === 'text'; + // Parsed once per message: for current messages (extraMetadata + // present, or agent messages) msg.content already has no markers, + // so this is a no-op. For legacy persisted user messages with raw + // [IMAGE:...]/[FILE:...] markers and no extraMetadata, this is + // what keeps the marker text out of both the rendered bubble and + // the copy-to-clipboard action. + const parsedContent = parseMessageImages(msg.content ?? ''); return (
{(() => { + const displayText = parsedContent.text; const dataUris = Array.isArray(msg.extraMetadata?.attachmentDataUris) ? (msg.extraMetadata.attachmentDataUris as string[]) - : parseMessageImages(msg.content ?? '').dataUris; + : parsedContent.dataUris; const hasImages = dataUris.length > 0; // Document attachments carry no image data-URI (only // images do); surface them as filename chips from the @@ -1953,14 +1961,14 @@ const Conversations = ({ ))}
)} - {(msg.content || showTime) && ( + {(displayText || showTime) && (
- {msg.content && ( - + {displayText && ( + )} {showTime && (

+ className={`${displayText ? 'mt-1' : ''} text-[10px] text-white/60`}> {formatRelativeTime(msg.createdAt)}

)} @@ -1974,7 +1982,7 @@ const Conversations = ({