diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index 12581ac6e8..784ec144e2 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -26,6 +26,7 @@ import PendingManualSkillsChips from './PendingManualSkillsChips'; import useAskAnswerMode from '~/hooks/Input/useAskAnswerMode'; import AskUserQuestionPopover from './AskUserQuestionPopover'; import { cn, getModelSpec, removeFocusRings } from '~/utils'; +import InterruptSteerButton from './InterruptSteerButton'; import DuringRunSendButton from './DuringRunSendButton'; import { useGetStartupConfig } from '~/data-provider'; import { mainTextareaId, BadgeItem } from '~/common'; @@ -344,13 +345,16 @@ const ChatForm = memo(function ChatForm({ ); /** ⌘/Ctrl+Enter = the non-default during-run action, ⌥/Alt+Enter = - * interrupt & send — the counterpart of Enter's `submitDuringRun`. */ + * interrupt & send (discards the answer), ⌘/Ctrl+Shift+Enter = interrupt & + * steer (keeps it) — all counterparts of Enter's `submitDuringRun`. */ const handleDuringRunModifier = useCallback( - (kind: 'other' | 'interrupt') => { + (kind: 'other' | 'interrupt' | 'preempt') => { const text = methods.getValues('text'); let consumed = false; if (kind === 'interrupt') { consumed = steering.interruptAndSend(text); + } else if (kind === 'preempt') { + consumed = steering.interruptSteer(text); } else if (steering.effectiveAction === 'steer') { consumed = steering.queueFromComposer(text); } else { @@ -661,6 +665,16 @@ const ChatForm = memo(function ChatForm({ isSubmitting={isSubmitting} /> )} + {steering.duringRunActive && (textValue?.trim() ?? '') !== '' && ( +
+ methods.getValues('text')} + onConsumed={() => methods.reset()} + disabled={filesLoading} + /> +
+ )}
{isSubmitting && showStopButton && !answerMode.active ? duringRunSlot diff --git a/client/src/components/Chat/Input/DuringRunSendButton.tsx b/client/src/components/Chat/Input/DuringRunSendButton.tsx index 01f6d57fa1..b2e5eae86c 100644 --- a/client/src/components/Chat/Input/DuringRunSendButton.tsx +++ b/client/src/components/Chat/Input/DuringRunSendButton.tsx @@ -2,7 +2,7 @@ import React, { forwardRef } from 'react'; import * as Ariakit from '@ariakit/react'; import { useWatch } from 'react-hook-form'; import { SendIcon } from '@librechat/client'; -import { Zap, Clock, OctagonPause } from 'lucide-react'; +import { Zap, Clock, OctagonPause, ZapOff } from 'lucide-react'; import type { Control } from 'react-hook-form'; import type { SteeringControls } from '~/hooks/Chat/useSteering'; import { isMacPlatform } from '~/utils/shortcuts'; @@ -43,8 +43,10 @@ type DuringRunSendButtonProps = { * (and `submitButtonRef`, so Enter's synthetic click routes here) whenever the * composer holds text — submitting steers or queues per the effective action. * Hovering it reveals the full action list with its shortcuts: steer, queue - * (⌘/Ctrl+Enter routes to the non-default action), and interrupt & send - * (⌥/Alt+Enter). Clearing the composer restores the Stop button. + * (⌘/Ctrl+Enter routes to the non-default action), interrupt & steer + * (⌘/Ctrl+Shift+Enter — stops writing now but keeps what is written), and + * interrupt & send (⌥/Alt+Enter — discards the answer and starts over). + * Clearing the composer restores the Stop button. */ const DuringRunSendButton = React.memo( forwardRef((props: DuringRunSendButtonProps, ref: React.ForwardedRef) => { @@ -55,6 +57,7 @@ const DuringRunSendButton = React.memo( const primary = steering.effectiveAction; const modEnter = isMacPlatform ? '⌘⏎' : 'Ctrl ⏎'; const altEnter = isMacPlatform ? '⌥⏎' : 'Alt ⏎'; + const modShiftEnter = isMacPlatform ? '⌘⇧⏎' : 'Ctrl ⇧ ⏎'; const runAction = (action: (text: string) => boolean | void) => { const text = props.getText().trim(); @@ -83,6 +86,14 @@ const DuringRunSendButton = React.memo( icon: