diff --git a/client/src/components/Chat/Messages/Content/Container.tsx b/client/src/components/Chat/Messages/Content/Container.tsx
index 81f97acf68..8a4806d74d 100644
--- a/client/src/components/Chat/Messages/Content/Container.tsx
+++ b/client/src/components/Chat/Messages/Content/Container.tsx
@@ -9,10 +9,10 @@ const Container = ({ children, message }: { children: React.ReactNode; message?:
dir="auto"
>
{message?.isCreatedByUser === true && }
- {/* Not user-only: this is the sole consumer of `message.files`, and an
- imported assistant message can carry attachments — voice-mode audio —
- that no content part renders. */}
- {message?.files != null && message.files.length > 0 && }
+ {/* Not user-only: an assistant message can carry attachments — voice-mode
+ audio — that no content part renders. `ContentParts` renders the same
+ files for messages that never reach this container. */}
+ {message?.files != null && message.files.length > 0 && }
{message?.isCreatedByUser === true && (
<>
diff --git a/client/src/components/Chat/Messages/Content/ContentParts.tsx b/client/src/components/Chat/Messages/Content/ContentParts.tsx
index 5b60b534c2..d9d56f8715 100644
--- a/client/src/components/Chat/Messages/Content/ContentParts.tsx
+++ b/client/src/components/Chat/Messages/Content/ContentParts.tsx
@@ -4,6 +4,7 @@ import type {
TMessageContentParts,
SearchResultData,
TAttachment,
+ TMessage,
Agents,
} from 'librechat-data-provider';
import type { ToolCallGroupExpansionState } from './ToolCallGroup';
@@ -16,6 +17,7 @@ import ApprovalProvider from './ApprovalContext';
import MemoryArtifacts from './MemoryArtifacts';
import ToolCallGroup from './ToolCallGroup';
import Container from './Container';
+import Files from './Files';
import Part from './Part';
const getToolCallId = (part: TMessageContentParts): string =>
@@ -119,6 +121,15 @@ type ContentPartsProps = {
isLast: boolean;
isSubmitting: boolean;
isLatestMessage?: boolean;
+ /**
+ * The message's own attachments. Images among them already render as
+ * `image_file` parts, so only the rest are shown here — but the rest have
+ * no content part at all, and `Container` (the only other consumer of this
+ * field) is never reached by a message that has content. Without this, an
+ * assistant turn that carries both reasoning and a generated file renders
+ * the reasoning and silently drops the file.
+ */
+ files?: TMessage['files'];
edit?: boolean;
enterEdit?: (cancel?: boolean) => void | null | undefined;
siblingIdx?: number;
@@ -136,6 +147,7 @@ type ContentPartsProps = {
*/
const ContentParts = memo(function ContentParts({
edit,
+ files,
isLast,
content,
manualSkills,
@@ -152,6 +164,10 @@ const ContentParts = memo(function ContentParts({
createdAt,
}: ContentPartsProps) {
const attachmentMap = useMemo(() => mapAttachments(attachments ?? []), [attachments]);
+ const nonImageFiles = useMemo(
+ () => files?.filter((file) => file.type?.startsWith('image/') !== true),
+ [files],
+ );
const effectiveIsSubmitting = isLatestMessage ? isSubmitting : false;
const toolGroupExpansionRef = useRef(new Map());
const fallbackScopeRef = useRef({ messageId, scope: 0 });
@@ -407,6 +423,11 @@ const ContentParts = memo(function ContentParts({
+ {nonImageFiles != null && nonImageFiles.length > 0 && (
+
+
+
+ )}
{renderPendingSkills()}
{showEmptyCursor && (
diff --git a/client/src/components/Chat/Messages/Content/Files.tsx b/client/src/components/Chat/Messages/Content/Files.tsx
index 4e4d85060c..1cf58edd2a 100644
--- a/client/src/components/Chat/Messages/Content/Files.tsx
+++ b/client/src/components/Chat/Messages/Content/Files.tsx
@@ -4,14 +4,17 @@ import FileContainer from '~/components/Chat/Input/Files/FileContainer';
import FilePreviewDialog from './FilePreviewDialog';
import Image from './Image';
-const Files = ({ message }: { message?: TMessage }) => {
+/** Takes the file list rather than the message: the content-part renderer has
+ * to show the non-image half of it without handing the whole message object to
+ * a memoized subtree. */
+const Files = ({ files }: { files?: TMessage['files'] }) => {
const imageFiles = useMemo(() => {
- return message?.files?.filter((file) => file.type?.startsWith('image/')) || [];
- }, [message?.files]);
+ return files?.filter((file) => file.type?.startsWith('image/')) || [];
+ }, [files]);
const otherFiles = useMemo(() => {
- return message?.files?.filter((file) => !file.type?.startsWith('image/')) || [];
- }, [message?.files]);
+ return files?.filter((file) => !file.type?.startsWith('image/')) || [];
+ }, [files]);
const [selectedFile, setSelectedFile] = useState | null>(null);
diff --git a/client/src/components/Chat/Messages/MessageParts.tsx b/client/src/components/Chat/Messages/MessageParts.tsx
index 4798bcd9a0..4ce57efaf4 100644
--- a/client/src/components/Chat/Messages/MessageParts.tsx
+++ b/client/src/components/Chat/Messages/MessageParts.tsx
@@ -146,6 +146,7 @@ function MessageParts(props: TMessageProps) {