From 0616aa4bf25ccbee01b536648b2ce043cd185d8a Mon Sep 17 00:00:00 2001
From: Marco Beretta <81851188+berry-13@users.noreply.github.com>
Date: Sun, 26 Jul 2026 14:34:19 +0200
Subject: [PATCH] refactor: keep only staged context in the tray
---
client/src/components/Chat/Input/ChatForm.tsx | 5 -
.../components/Chat/Input/Composer/Tray.tsx | 237 +-----------------
.../Input/Composer/__tests__/Tray.spec.tsx | 112 +--------
3 files changed, 11 insertions(+), 343 deletions(-)
diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx
index a12e816689..b30e74e42d 100644
--- a/client/src/components/Chat/Input/ChatForm.tsx
+++ b/client/src/components/Chat/Input/ChatForm.tsx
@@ -587,16 +587,11 @@ const ChatForm = memo(function ChatForm({
{endpoint && (
diff --git a/client/src/components/Chat/Input/Composer/Tray.tsx b/client/src/components/Chat/Input/Composer/Tray.tsx
index 11c87fdc57..c2c04b9c7e 100644
--- a/client/src/components/Chat/Input/Composer/Tray.tsx
+++ b/client/src/components/Chat/Input/Composer/Tray.tsx
@@ -1,19 +1,11 @@
import { memo } from 'react';
-import { useRecoilValue } from 'recoil';
-import { Zap, Clock, Pencil, Trash2, RotateCcw, TextQuote, WandSparkles } from 'lucide-react';
-import type { TMessage, TConversation } from 'librechat-data-provider';
-import type { SteeringControls, QueuedMessageContext } from '~/hooks/Chat/useSteering';
+import { TextQuote, WandSparkles } from 'lucide-react';
+import type { TConversation } from 'librechat-data-provider';
import type { ComposerItem, ComposerItemKind } from '~/hooks/Input/useComposerItems';
-import type { PendingSteer, QueuedMessage } from '~/store/families';
-import type { RestoreToComposer } from '../InFlightSteers';
import type { ExtendedFile, FileSetter } from '~/common';
-import type { MenuEntry } from '../SteerMenu';
-import { RowMenu, useDefaultToggleEntry, ICON_BTN_CLASS, PRIMARY_BTN_CLASS } from '../SteerMenu';
-import { useFileHandlingNoChatContext } from '~/hooks';
+import { useFileHandlingNoChatContext, useLocalize } from '~/hooks';
import Chip, { type ChipTone } from './Chip';
import FileRow from '../Files/FileRow';
-import { useLocalize } from '~/hooks';
-import store from '~/store';
const KIND_ICON: Record
= {
quote: ,
@@ -41,215 +33,28 @@ function ItemChip({ item }: { item: ComposerItem }) {
);
}
-function QueuedChip({
- message,
- steering,
- conversationId,
- onEditToComposer,
- onRestoreToComposer,
-}: {
- message: QueuedMessage;
- steering: SteeringControls;
- conversationId: string;
- onEditToComposer: (
- text: string,
- files?: TMessage['files'],
- context?: QueuedMessageContext,
- ) => void;
- onRestoreToComposer: RestoreToComposer;
-}) {
- const localize = useLocalize();
- const toggleEntry = useDefaultToggleEntry(steering);
- const canSteerNow = steering.duringRunActive && steering.canSteer;
- const showPrimary = canSteerNow || !steering.duringRunActive;
-
- const entries: MenuEntry[] = [
- {
- key: 'edit',
- label: localize('com_ui_edit_message'),
- icon: ,
- onClick: () => {
- steering.removeQueued(message.id);
- onEditToComposer(message.text, message.files, {
- quotes: message.quotes,
- manualSkills: message.manualSkills,
- });
- },
- },
- toggleEntry,
- ];
-
- return (
- }
- data-testid="queued-message-row"
- trailing={
- <>
- {showPrimary && (
-
- )}
-
-
- >
- }
- />
- );
-}
-
-function FailedSteerChip({
- steer,
- steering,
- onEditToComposer,
-}: {
- steer: PendingSteer;
- steering: SteeringControls;
- onEditToComposer: (
- text: string,
- files?: TMessage['files'],
- context?: QueuedMessageContext,
- ) => void;
-}) {
- const localize = useLocalize();
- const toggleEntry = useDefaultToggleEntry(steering);
-
- const entries: MenuEntry[] = [
- {
- key: 'edit',
- label: localize('com_ui_edit_message'),
- icon: ,
- onClick: () => {
- steering.removeSteer(steer.steerId);
- onEditToComposer(steer.text, steer.files, {
- quotes: steer.quotes,
- manualSkills: steer.manualSkills,
- });
- },
- },
- {
- key: 'queue',
- label: localize('com_ui_convert_to_queue'),
- icon: ,
- onClick: () =>
- steering.convertSteerToQueue(steer.steerId, steer.text, steer.files, {
- quotes: steer.quotes,
- manualSkills: steer.manualSkills,
- }),
- },
- toggleEntry,
- ];
-
- return (
- }
- data-testid="steer-message-row"
- onRemove={() => steering.removeSteer(steer.steerId)}
- removeLabel={localize('com_ui_remove_queued')}
- trailing={
- <>
- {localize('com_ui_steer_failed')}
-
-
- >
- }
- />
- );
-}
-
interface TrayProps {
items: ComposerItem[];
- conversationId: string;
conversation: TConversation | null;
/** Staged uploads, rendered by `FileRow` with their own delete semantics. */
files: Map;
setFiles: FileSetter;
setFilesLoading: React.Dispatch>;
isRTL: boolean;
- steering: SteeringControls;
- steeringEnabled: boolean;
- onEditToComposer: (
- text: string,
- files?: TMessage['files'],
- context?: QueuedMessageContext,
- ) => void;
- onRestoreToComposer: RestoreToComposer;
}
/**
- * The composer's single staged-context region. Replaces the four stacked rows
- * (uploads, quotes, manual skills, queued/failed steers) with one region: file
- * cards first, then the pills, then the queued and failed rows — which carry
- * their own primary action and overflow menu — full width beneath them.
+ * The composer's staged-context region: file cards first, then the quote and
+ * manual-skill pills. Queued and failed-steer rows live elsewhere (the Queue
+ * rail and the thread, respectively).
*
* Files keep `FileRow` rather than being flattened into pills: the card is
* where an image preview opens full size, where upload progress and the storage
* source are drawn, and where aborting a part-done upload differs from deleting
* a finished one.
*/
-function Tray({
- items,
- conversationId,
- conversation,
- files,
- setFiles,
- setFilesLoading,
- isRTL,
- steering,
- steeringEnabled,
- onEditToComposer,
- onRestoreToComposer,
-}: TrayProps) {
+function Tray({ items, conversation, files, setFiles, setFilesLoading, isRTL }: TrayProps) {
const localize = useLocalize();
- const steers = useRecoilValue(store.pendingSteersByConvoId(conversationId));
- const queued = useRecoilValue(store.queuedMessagesByConvoId(steering.queueKey));
const { abortUpload } = useFileHandlingNoChatContext(undefined, {
files,
setFiles,
@@ -257,15 +62,7 @@ function Tray({
conversation,
});
- const failedSteers = steeringEnabled ? steers.filter((steer) => steer.status === 'failed') : [];
- const queuedMessages = steeringEnabled ? queued : [];
-
- if (
- items.length === 0 &&
- files.size === 0 &&
- failedSteers.length === 0 &&
- queuedMessages.length === 0
- ) {
+ if (items.length === 0 && files.size === 0) {
return null;
}
@@ -290,24 +87,6 @@ function Tray({
))}
)}
- {failedSteers.map((steer) => (
-