From 8135c808dfedc0da6dcd9ae84aa7c5ad4dba4ed0 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 16 Jul 2026 08:35:36 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20fix:=20Match=20In-Flight=20Steer?= =?UTF-8?q?=20Presentation=20to=20the=20Applied=20Part?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review on 6a5f36f7ef. All three findings were real, and all three were the same underlying mistake: the anchored bubble hand-rolled presentation instead of reusing the leaves the applied SteerPart uses, so a steer visibly changed on apply. - Images: the message `Image` sets an inline height from the file's dimensions and centers with object-contain, so clipping it into a 56px wrapper showed the blank top of a large element. Use ImagePreview, the composer's fixed-size thumbnail path (also gives click-to-enlarge). - Non-image files: FileContainer always renders a button, so without an onClick the chip was dead. Wire FilePreviewDialog, as SteerPart does. - Markdown: honor enableUserMsgMarkdown so text does not reflow the moment the server injects it. Splits files in a single pass rather than two filters. --- .../components/Chat/Input/InFlightSteers.tsx | 87 +++++++++++++----- .../Input/__tests__/InFlightSteers.test.tsx | 91 +++++++++++++++++-- 2 files changed, 146 insertions(+), 32 deletions(-) diff --git a/client/src/components/Chat/Input/InFlightSteers.tsx b/client/src/components/Chat/Input/InFlightSteers.tsx index edf05728fc..207c16da78 100644 --- a/client/src/components/Chat/Input/InFlightSteers.tsx +++ b/client/src/components/Chat/Input/InFlightSteers.tsx @@ -1,14 +1,25 @@ -import { memo } from 'react'; +import { memo, useMemo, useState, useCallback } from 'react'; import { X, Zap } from 'lucide-react'; import { useRecoilValue } from 'recoil'; -import type { TFile } from 'librechat-data-provider'; +import type { TFile, TMessage } from 'librechat-data-provider'; import type { PendingSteer } from '~/store/families'; +import FilePreviewDialog from '~/components/Chat/Messages/Content/FilePreviewDialog'; +import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite'; import FileContainer from '~/components/Chat/Input/Files/FileContainer'; -import Image from '~/components/Chat/Messages/Content/Image'; +import ImagePreview from '~/components/Chat/Input/Files/ImagePreview'; import { useSteerCancel, useLocalize } from '~/hooks'; import { cn } from '~/utils'; import store from '~/store'; +const splitFiles = (files?: TMessage['files']) => { + const images: NonNullable = []; + const others: NonNullable = []; + for (const file of files ?? []) { + (file.type?.startsWith('image/') === true ? images : others).push(file); + } + return { images, others }; +}; + /** * One steer on its way into the run, anchored above the composer as a message * bubble rather than a control chip — the words are already part of the @@ -16,6 +27,10 @@ import store from '~/store'; * `on_steer_applied`, when the persisted STEER part lands at its authoritative * position in the response. * + * Text and attachments render through the same leaves as the applied + * `SteerPart` (markdown toggle, file preview) so the words don't reformat the + * moment the server injects them. + * * `sending` is still awaiting its 202 ACK (no server id yet, so nothing to * cancel); `pending` is acknowledged and waiting on the next tool-batch * boundary. @@ -29,8 +44,16 @@ const InFlightSteer = memo(function InFlightSteer({ }) { const localize = useLocalize(); const cancelSteer = useSteerCancel(conversationId); + const enableUserMsgMarkdown = useRecoilValue(store.enableUserMsgMarkdown); + const [selectedFile, setSelectedFile] = useState | null>(null); + const handlePreviewClose = useCallback((open: boolean) => { + if (!open) { + setSelectedFile(null); + } + }, []); + + const { images, others } = useMemo(() => splitFiles(steer.files), [steer.files]); const sending = steer.status === 'sending'; - const files = steer.files ?? []; return (
- {files.length > 0 && ( -
- {files.map((file) => - file.type?.startsWith('image/') === true ? ( -
- -
- ) : ( - - ), - )} + {(images.length > 0 || others.length > 0) && ( +
+ {others.map((file) => ( + setSelectedFile(file)} + /> + ))} + {images.map((file) => ( +
+ +
+ ))}
)}
- {steer.text} + {enableUserMsgMarkdown ? : steer.text}
)}
+ {others.length > 0 && ( + + )}
); }); @@ -101,7 +140,7 @@ const InFlightSteers = memo(function InFlightSteers({ }) { const localize = useLocalize(); const steers = useRecoilValue(store.pendingSteersByConvoId(conversationId)); - const inFlight = steers.filter((steer) => steer.status !== 'failed'); + const inFlight = useMemo(() => steers.filter((steer) => steer.status !== 'failed'), [steers]); if (inFlight.length === 0) { return null; diff --git a/client/src/components/Chat/Input/__tests__/InFlightSteers.test.tsx b/client/src/components/Chat/Input/__tests__/InFlightSteers.test.tsx index 3cc19eefae..e8b4cf71ba 100644 --- a/client/src/components/Chat/Input/__tests__/InFlightSteers.test.tsx +++ b/client/src/components/Chat/Input/__tests__/InFlightSteers.test.tsx @@ -18,23 +18,45 @@ jest.mock('~/data-provider', () => ({ jest.mock('~/components/Chat/Input/Files/FileContainer', () => ({ __esModule: true, - default: ({ file }: { file: { filename?: string } }) => ( -
{file.filename}
+ default: ({ file, onClick }: { file: { filename?: string }; onClick?: () => void }) => ( + ), })); -jest.mock('~/components/Chat/Messages/Content/Image', () => ({ +/** The composer thumbnail path: a fixed-size button painted with a background + * image, not an — assert on the url it was handed. */ +jest.mock('~/components/Chat/Input/Files/ImagePreview', () => ({ __esModule: true, - default: ({ altText }: { altText: string }) => {altText}, + default: ({ url, alt }: { url?: string; alt?: string }) => ( +