diff --git a/client/src/components/Chat/Input/InFlightSteers.tsx b/client/src/components/Chat/Input/InFlightSteers.tsx index 8de9161ab4..393ed16724 100644 --- a/client/src/components/Chat/Input/InFlightSteers.tsx +++ b/client/src/components/Chat/Input/InFlightSteers.tsx @@ -7,7 +7,12 @@ import type { TFile, TMessage } from 'librechat-data-provider'; import type { SteeringControls, QueuedMessageContext } from '~/hooks/Chat/useSteering'; import type { PendingSteer } from '~/store/families'; import type { MenuEntry } from './SteerMenu'; -import { RowMenu, useDefaultToggleEntry, useInterruptToggleEntry } from './SteerMenu'; +import { + RowMenu, + EscalateNowButton, + useDefaultToggleEntry, + useInterruptToggleEntry, +} from './SteerMenu'; import FilePreviewDialog from '~/components/Chat/Messages/Content/FilePreviewDialog'; import { steerOverlayHeightFamily, escalatingSteerFamily } from '~/store/steer'; import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite'; @@ -144,6 +149,53 @@ const InFlightSteer = memo(function InFlightSteer({ const { mutateAsync: armSteer } = useArmSteerMutation(); const setEscalating = useSetAtom(escalatingSteerFamily(conversationId)); + /** + * Escalate this waiting steer to an interrupt: ONE atomic server op flips + * `preempt` on the EXISTING queued item, so its FIFO position, id, and + * timestamp survive and there is no reclaim window to race. Every "too + * late" interleaving (drained, cancelled, run ended or replaced) is the + * same honest `armed: false`, and the chip is only relabelled on a + * confirmed durable arm. The escalating flag flips synchronously, before + * the request: the chip-derived gate cannot see this arm until the + * response lands, and the other escalation controls advertise "one + * interrupt at a time". + */ + const escalate = useCallback(() => { + setEscalating(true); + void armSteer({ conversationId, steerId: steer.steerId }) + .then( + (response) => { + if (response.armed === true) { + markSteerPreempt(steer.steerId); + return; + } + /* `armed: false` is deliberately ambiguous — injected, cancelled, + * re-homed, or run over — so the message only says the escalation + * lost, and the chip defers to the events for what happened. */ + showToast({ + message: localize( + response.code === 'PREEMPT_UNSUPPORTED' + ? 'com_ui_steer_preempt_unsupported' + : 'com_ui_steer_arm_lost_race', + ), + status: 'info', + }); + }, + () => { + showToast({ message: localize('com_ui_steer_arm_failed'), status: 'error' }); + }, + ) + .finally(() => setEscalating(false)); + }, [ + armSteer, + conversationId, + steer.steerId, + setEscalating, + markSteerPreempt, + showToast, + localize, + ]); + /** * Takes the steer back off the server queue so its words can be re-homed. * The chip is left alone until the answer is known: only `reclaimed` proves @@ -198,82 +250,6 @@ const InFlightSteer = memo(function InFlightSteer({ }); }, }, - { - key: 'queue', - label: localize('com_ui_convert_to_queue'), - icon: