diff --git a/client/src/hooks/Files/__tests__/useFileDeletion.spec.tsx b/client/src/hooks/Files/__tests__/useFileDeletion.spec.tsx
new file mode 100644
index 0000000000..da084426c8
--- /dev/null
+++ b/client/src/hooks/Files/__tests__/useFileDeletion.spec.tsx
@@ -0,0 +1,135 @@
+import React, { useState } from 'react';
+import { FileSources, EToolResources } from 'librechat-data-provider';
+import { render, screen, act, renderHook } from '@testing-library/react';
+import type { ExtendedFile } from '~/common';
+import FileRow from '~/components/Chat/Input/Files/FileRow';
+import useFileDeletion from '../useFileDeletion';
+
+const mockMutateAsync = jest.fn();
+
+jest.mock('~/data-provider', () => ({
+ useDeleteFilesMutation: () => ({ mutateAsync: mockMutateAsync }),
+}));
+
+jest.mock('@librechat/client', () => ({
+ useToastContext: () => ({ showToast: jest.fn() }),
+}));
+
+jest.mock('~/hooks', () => ({
+ useLocalize: () => (key: string) => key,
+}));
+
+jest.mock('~/utils', () => ({
+ ...jest.requireActual('~/utils'),
+ logger: { log: jest.fn() },
+ getCachedPreview: () => undefined,
+ deletePreview: jest.fn(),
+}));
+
+jest.mock('../useSetFilesToDelete', () => ({
+ __esModule: true,
+ default: () => jest.fn(),
+}));
+
+jest.mock('~/components/Chat/Input/Files/FileContainer', () => {
+ return function MockFileContainer({
+ file,
+ onDelete,
+ }: {
+ file: ExtendedFile;
+ onDelete: () => void;
+ }) {
+ return ;
+ };
+});
+
+jest.mock('~/components/Chat/Input/Files/Image', () => {
+ return function MockImage() {
+ return null;
+ };
+});
+
+/** Mirrors the shape `utils/forms.tsx` builds for agent Context/File Search panels */
+const makeFile = (file_id: string): ExtendedFile =>
+ ({
+ file_id,
+ type: 'application/pdf',
+ filepath: `/uploads/${file_id}.pdf`,
+ filename: `${file_id}.pdf`,
+ size: 1024,
+ progress: 1,
+ source: FileSources.local,
+ }) as ExtendedFile;
+
+/** Mirrors FileContext.tsx, which mounts FileRow only while `fileCount > 0` */
+function ConditionalPanel({ initial }: { initial: ExtendedFile[] }) {
+ const [files, setFiles] = useState(new Map(initial.map((f) => [f.file_id, f])));
+ return (
+ <>
+ {files.size > 0 && (
+
+ )}
+ >
+ );
+}
+
+describe('useFileDeletion', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ mockMutateAsync.mockClear();
+ });
+ afterEach(() => jest.useRealTimers());
+
+ const clickDelete = (file_id: string) => {
+ act(() => {
+ screen.getByTestId(`del-${file_id}`).click();
+ });
+ act(() => {
+ jest.advanceTimersByTime(3000);
+ });
+ };
+
+ it('sends the unlink request when removing the last file from an agent panel', () => {
+ render();
+
+ clickDelete('only-file');
+
+ expect(mockMutateAsync).toHaveBeenCalledTimes(1);
+ expect(mockMutateAsync).toHaveBeenCalledWith({
+ files: [expect.objectContaining({ file_id: 'only-file' })],
+ agent_id: 'agent-123',
+ tool_resource: EToolResources.context,
+ });
+ });
+
+ it('sends the unlink request when removing a non-last file from an agent panel', () => {
+ render();
+
+ clickDelete('file-a');
+
+ expect(mockMutateAsync).toHaveBeenCalledTimes(1);
+ expect(mockMutateAsync).toHaveBeenCalledWith(
+ expect.objectContaining({ agent_id: 'agent-123', tool_resource: EToolResources.context }),
+ );
+ });
+
+ it('does not delete a file that was attached from existing storage', () => {
+ const { result } = renderHook(() => useFileDeletion({ mutateAsync: mockMutateAsync }));
+
+ act(() => {
+ result.current.deleteFile({
+ file: { ...makeFile('stored-file'), attached: true } as ExtendedFile,
+ });
+ });
+ act(() => {
+ jest.advanceTimersByTime(3000);
+ });
+
+ expect(mockMutateAsync).not.toHaveBeenCalled();
+ });
+});
diff --git a/client/src/hooks/Files/useFileDeletion.ts b/client/src/hooks/Files/useFileDeletion.ts
index c33ac2a50b..18a189b9ac 100644
--- a/client/src/hooks/Files/useFileDeletion.ts
+++ b/client/src/hooks/Files/useFileDeletion.ts
@@ -1,8 +1,8 @@
+import { useCallback, useState, useEffect } from 'react';
import debounce from 'lodash/debounce';
import { FileSources, EToolResources, removeNullishValues } from 'librechat-data-provider';
-import { useCallback, useState, useEffect } from 'react';
-import type * as t from 'librechat-data-provider';
import type { UseMutateAsyncFunction } from '@tanstack/react-query';
+import type * as t from 'librechat-data-provider';
import type { ExtendedFile, GenericSetter } from '~/common';
import useSetFilesToDelete from './useSetFilesToDelete';
import { deletePreview } from '~/utils';
@@ -51,8 +51,9 @@ const useFileDeletion = ({
const debouncedDelete = useCallback(debounce(executeBatchDelete, 1000), []);
useEffect(() => {
- // Cleanup function for debouncedDelete when component unmounts or before re-render
- return () => debouncedDelete.cancel();
+ /** Flush, don't cancel: unmount is a normal outcome of removing the last file,
+ * and a cancelled batch silently drops a delete the user already confirmed. */
+ return () => debouncedDelete.flush();
}, [debouncedDelete]);
const deleteFile = useCallback(