From 1085ff983e2406ec707f1bbb68da4eecf85a1357 Mon Sep 17 00:00:00 2001 From: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:33:05 +0530 Subject: [PATCH 1/2] FIX: revoke removed attachment blob URLs in the conversation editor The chat composer revokes a dropped attachment's blob object URL when the attachment is removed, but the conversation editor forwarded the new attachment list straight through, so removed files stayed pinned until the draft was discarded or the editor unmounted. Revoke the media piece's blob URL when the next list no longer references it. Fixes #3185 Signed-off-by: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com> --- .../components/Chat/ConversationEditor.test.tsx | 15 +++++++++++++++ .../src/components/Chat/ConversationEditor.tsx | 7 ++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Chat/ConversationEditor.test.tsx b/frontend/src/components/Chat/ConversationEditor.test.tsx index 7a557434c2..5d586f58b7 100644 --- a/frontend/src/components/Chat/ConversationEditor.test.tsx +++ b/frontend/src/components/Chat/ConversationEditor.test.tsx @@ -289,3 +289,18 @@ describe('ConversationEditor', () => { expect(onSaved).not.toHaveBeenCalled() }) }) + +it('revokes the blob URL when an attachment is removed from the editor', async () => { + const user = userEvent.setup() + const mockObjectUrl = 'blob:http://localhost/attach-1' + global.URL.createObjectURL = jest.fn().mockReturnValue(mockObjectUrl) + global.URL.revokeObjectURL = jest.fn() + setupEditor() + + const first = screen.getByRole('region', { name: 'Message 1' }) + await user.upload(within(first).getAllByTestId('file-input')[0], new File(['x'], 'photo.png', { type: 'image/png' })) + + await user.click(await screen.findByRole('button', { name: 'Remove photo.png' })) + + expect(global.URL.revokeObjectURL).toHaveBeenCalledWith(mockObjectUrl) +}) diff --git a/frontend/src/components/Chat/ConversationEditor.tsx b/frontend/src/components/Chat/ConversationEditor.tsx index 33c01c212e..9e132a6ad6 100644 --- a/frontend/src/components/Chat/ConversationEditor.tsx +++ b/frontend/src/components/Chat/ConversationEditor.tsx @@ -101,8 +101,13 @@ function DraftPieceEditor({ piece, disabled, onChange, onAttachments, onConverte }) }, [media, onChange, piece.draftId]) const changeAttachments = useCallback((attachments: MessageAttachment[]): void => { + if (media) { + const nextUrls = new Set(attachments.map((attachment: MessageAttachment) => attachment.url)) + const current = draftAttachment(piece) + if (!nextUrls.has(current.url) && current.url.startsWith('blob:')) URL.revokeObjectURL(current.url) + } onAttachments(piece.draftId, attachments) - }, [onAttachments, piece.draftId]) + }, [media, piece, onAttachments, piece.draftId]) const clearConversion = (): void => onChange(piece.draftId, (current: ConversationDraftPiece) => ({ ...current, converted_value: undefined, converted_value_data_type: undefined, applied_converter_ids: [], })) From bb8a9187000822d4f09b1c265a1f50b9d7507cb1 Mon Sep 17 00:00:00 2001 From: Roman Lutz Date: Tue, 13 Oct 2026 23:28:46 -0700 Subject: [PATCH 2/2] FIX: remove redundant editor callback dependency Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 49f13576-5cf7-4b3d-beba-39e00c34aca4 --- frontend/src/components/Chat/ConversationEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Chat/ConversationEditor.tsx b/frontend/src/components/Chat/ConversationEditor.tsx index 9e132a6ad6..c52b004046 100644 --- a/frontend/src/components/Chat/ConversationEditor.tsx +++ b/frontend/src/components/Chat/ConversationEditor.tsx @@ -107,7 +107,7 @@ function DraftPieceEditor({ piece, disabled, onChange, onAttachments, onConverte if (!nextUrls.has(current.url) && current.url.startsWith('blob:')) URL.revokeObjectURL(current.url) } onAttachments(piece.draftId, attachments) - }, [media, piece, onAttachments, piece.draftId]) + }, [media, piece, onAttachments]) const clearConversion = (): void => onChange(piece.draftId, (current: ConversationDraftPiece) => ({ ...current, converted_value: undefined, converted_value_data_type: undefined, applied_converter_ids: [], }))