diff --git a/client/src/components/Chat/Input/Composer/Bar.tsx b/client/src/components/Chat/Input/Composer/Bar.tsx
index 6fe4292404..c6bc5244c0 100644
--- a/client/src/components/Chat/Input/Composer/Bar.tsx
+++ b/client/src/components/Chat/Input/Composer/Bar.tsx
@@ -428,7 +428,12 @@ function Bar({
{dictating ? (
@@ -442,6 +447,7 @@ function Bar({
primary
label={localize('com_nav_send_message')}
onClick={dictation.stopAndSend}
+ disabled={dictation.transcribing}
>
diff --git a/client/src/components/Chat/Input/Composer/Queue.tsx b/client/src/components/Chat/Input/Composer/Queue.tsx
index 802147d670..9e84e99115 100644
--- a/client/src/components/Chat/Input/Composer/Queue.tsx
+++ b/client/src/components/Chat/Input/Composer/Queue.tsx
@@ -1,8 +1,8 @@
import { memo, useRef, useMemo, useState, useCallback } from 'react';
import { useRecoilValue } from 'recoil';
import { useDrag, useDrop } from 'react-dnd';
-import { useMediaQuery } from '@librechat/client';
import { X, Pencil, GripVertical } from 'lucide-react';
+import { useMediaQuery, useToastContext } from '@librechat/client';
import type { TMessage } from 'librechat-data-provider';
import type { SteeringControls, QueuedMessageContext } from '~/hooks/Chat/useSteering';
import type { QueuedMessage } from '~/store/families';
@@ -71,6 +71,7 @@ function QueueRow({
onAnnounce,
}: QueueRowProps) {
const localize = useLocalize();
+ const { showToast } = useToastContext();
const rowRef = useRef(null);
const gripRef = useRef(null);
const { reorderQueued, restoreQueuedOrder } = steering;
@@ -241,7 +242,15 @@ function QueueRow({
);
if (restored) {
steering.removeQueued(message.id);
+ return;
}
+ /* Refusing silently reads as a dead button: the row stays, nothing
+ moves, and the reason (a draft in the box, another chat on screen)
+ is somewhere the click was not. */
+ showToast({
+ message: localize('com_ui_queue_remove_blocked'),
+ status: 'warning',
+ });
}}
className={ICON_BTN}
>
diff --git a/client/src/components/Chat/Input/Composer/__tests__/Queue.spec.tsx b/client/src/components/Chat/Input/Composer/__tests__/Queue.spec.tsx
index 2067f33593..fb49d41014 100644
--- a/client/src/components/Chat/Input/Composer/__tests__/Queue.spec.tsx
+++ b/client/src/components/Chat/Input/Composer/__tests__/Queue.spec.tsx
@@ -18,6 +18,12 @@ jest.mock('~/hooks', () => ({
},
}));
+const mockShowToast = jest.fn();
+jest.mock('@librechat/client', () => ({
+ ...jest.requireActual('@librechat/client'),
+ useToastContext: () => ({ showToast: mockShowToast }),
+}));
+
const CONVO_ID = 'convo-1';
const mockSendQueuedNow = jest.fn();
const mockRemoveQueued = jest.fn();
@@ -184,6 +190,11 @@ describe('Queue', () => {
fireEvent.click(screen.getByLabelText('com_ui_remove_queued'));
expect(onRestore).toHaveBeenCalled();
expect(mockRemoveQueued).not.toHaveBeenCalled();
+ /* Keeping the words is right; saying nothing about it is not. Without this
+ the row simply does not react and the button reads as broken. */
+ expect(mockShowToast).toHaveBeenCalledWith(
+ expect.objectContaining({ message: 'com_ui_queue_remove_blocked' }),
+ );
});
it('hands the whole message to the composer to edit', () => {
diff --git a/client/src/hooks/Chat/useSteering.ts b/client/src/hooks/Chat/useSteering.ts
index db33b828df..3f2f1a198d 100644
--- a/client/src/hooks/Chat/useSteering.ts
+++ b/client/src/hooks/Chat/useSteering.ts
@@ -5,7 +5,7 @@ import { useRecoilValue, useSetRecoilState, useRecoilCallback } from 'recoil';
import { Constants, ContentTypes, isAssistantsEndpoint } from 'librechat-data-provider';
import type { TMessage, TConversation, TMessageContentParts } from 'librechat-data-provider';
import type { CallbackInterface } from 'recoil';
-import type { RunEnd, PendingSteer, QueuedMessage } from '~/store/families';
+import type { PendingSteer, QueuedMessage } from '~/store/families';
import type { ExtendedFile, FileSetter } from '~/common';
import {
useGetMessagesByConvoId,
@@ -13,7 +13,6 @@ import {
useMarkFilesUsageMutation,
} from '~/data-provider';
import { carriedSteerContext, clearAllDrafts } from '~/utils';
-import useSteerConvert from '~/hooks/Chat/useSteerConvert';
import { useSetFilesToDelete } from '~/hooks/Files';
import useLocalize from '~/hooks/useLocalize';
import store from '~/store';
@@ -165,7 +164,6 @@ export default function useSteering({
const localize = useLocalize();
const { showToast } = useToastContext();
const setFilesToDelete = useSetFilesToDelete();
- const convertSteersToQueued = useSteerConvert();
/** `mutate` is a stable callback; the mutation result objects are fresh
* every render and would defeat the memoized return value below. */
const { mutate: steerMessage } = useSteerMessageMutation();
@@ -205,31 +203,6 @@ export default function useSteering({
const isSubmittingRef = useRef(isSubmitting);
isSubmittingRef.current = isSubmitting;
- /**
- * How each conversation's last run ended, kept because `useQueueDrain`
- * CONSUMES the one-shot signal — by the time a reclaim resolves it is already
- * gone. Every subscriber renders before the drain's effect nulls it, so the
- * outcome is captured first. Both carriers are watched: the index signal, and
- * the copy parked under the conversation when the run ended while the user
- * was looking elsewhere.
- *
- * Keyed by conversation because this hook is REUSED across chats: a single
- * slot would answer for whichever chat is on screen when the reclaim lands,
- * not the one the words belong to. An entry is dropped once that conversation
- * starts another run — an older end no longer describes what is happening.
- */
- const runEnd = useRecoilValue(store.runEndByIndex(index));
- const parkedRunEnd = useRecoilValue(store.pendingRunEndByConvoId(queueKey));
- const runEndsRef = useRef