Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f1b5202
feat: Refine the Ask Question Surfaces and Drop the Stop Shortcut Hint
berry-13 Oct 1, 2026
81c4530
feat: Smooth Step Changes in Batched Questions With Numbered Option Rows
berry-13 Oct 1, 2026
c9f98c8
fix: Swap Batched Question Answers Instantly While the Height Eases
berry-13 Oct 1, 2026
708eea3
fix: Keep the Batch Card's Place in the Thread and the Answer Placeho…
berry-13 Oct 1, 2026
a124f04
feat: Instant Option Row Hover and Typed Answers Replace Choices
berry-13 Oct 1, 2026
c84eee3
style: Fade Option Rows Slowly When They Lock
berry-13 Oct 1, 2026
561c75d
feat: Swap Batched Questions Instantly While Only the Height Eases
berry-13 Oct 1, 2026
7405e09
fix: Ease the Batch Status Lines and Keep Question Controls Unselectable
berry-13 Oct 1, 2026
85987cd
fix: Stop the Batch Answer Area From Showing a Scrollbar
berry-13 Oct 1, 2026
06b2a20
fix: Keep the Locked Composer Placeholder Out of Text Selection
berry-13 Oct 1, 2026
98efae2
feat: Check Mark on a Chosen Single-Select Answer
berry-13 Oct 1, 2026
8ba6167
feat: Turn the Question Move Chevron During the Composer and Chat Morph
berry-13 Oct 1, 2026
3095b73
fix: Ease Only the Batch Card's Outer Height When Switching Questions
berry-13 Oct 1, 2026
dd49b62
fix: Pin the Batch Header and Buttons to the Card Edges While Its Hei…
berry-13 Oct 1, 2026
fa8ed78
fix: Confirm Batch Answers on Enter, Bound Long Prompts, and Keep the…
berry-13 Oct 2, 2026
68eb900
test: Scenarios for Batched Ask Question Answers
berry-13 Oct 2, 2026
ffd22b2
test: Pause the Fake Model on a Real Question Batch for the Ask Scena…
berry-13 Oct 2, 2026
50bc54b
test: Run the Ask Scenarios in a Desktop Context
berry-13 Oct 2, 2026
18ffc96
fix: Cap the Batch Question Popover to the Viewport
berry-13 Oct 2, 2026
dbc0db8
fix: Scroll the Capped Batch Card, Guard IME Enter, and Keep Focus on…
berry-13 Oct 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 40 additions & 48 deletions client/src/components/Chat/Input/AskUserQuestionPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,45 +46,53 @@ function AskUserQuestionPopoverContent({
}

function AskUserQuestionsPopoverPanel({ ask }: { ask: ReturnType<typeof useAskAnswerMode> }) {
const localize = useLocalize();
const { liveAsk, collapse } = ask;
const questions = liveAsk?.questions;
if (liveAsk == null || questions == null || questions.length === 0) {
return null;
}

return (
<div className="absolute bottom-28 z-10 w-full">
<div className="popover border-border-light bg-surface-primary-alt flex max-h-[70vh] flex-col rounded-2xl border shadow-lg">
<div className="border-border-light flex shrink-0 items-center justify-between gap-2 border-b px-3 py-2">
<p className="text-text-primary text-sm font-medium">
{localize(
questions.length === 1 ? 'com_ui_asking_questions_one' : 'com_ui_asking_questions',
{ 0: questions.length },
)}
</p>
<TooltipAnchor
description={localize('com_ui_ask_move_to_chat')}
side="top"
render={
<Button
variant="ghost"
size="icon"
aria-label={localize('com_ui_ask_move_to_chat')}
className="text-text-secondary size-auto rounded-md p-1"
onClick={collapse}
>
<ChevronDown className="size-4" aria-hidden="true" />
</Button>
}
/>
</div>
<AskUserQuestions actionId={liveAsk.actionId} questions={questions} />
<div className="absolute bottom-full z-10 mb-2 w-full">
Comment thread
berry-13 marked this conversation as resolved.
{/* The prompt and answers each scroll within their own caps; this outer cap keeps
the whole card on screen when both are at their limit on a short viewport. */}
<div className="popover border-border-light bg-surface-secondary flex max-h-[70vh] flex-col overflow-y-auto rounded-2xl border shadow-lg [view-transition-name:ask-question]">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Let the capped batch card actually overflow

For a valid batch where the prompt reaches its 25vh cap and several long options make the answer region reach 45vh, those regions plus the nonshrinking footer exceed this 70vh wrapper. The current AskUserQuestions child is allowed to shrink (min-h-0), and its AutoHeight child uses overflow-hidden, so flex layout clips the bottom of the answer region rather than creating overflow for this wrapper to scroll; the final options or text input can therefore remain unreachable. Fresh evidence beyond the earlier long-prompt thread is the current combination of this outer cap with the shrinking, clipping AutoHeight; make that content nonshrinking or put the scroll boundary outside the clipping element.

AGENTS.md reference: AGENTS.md:L49-L52

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cb61b3a: the batch card is shrink-0 inside the 70vh popover, so the popover scrolls instead of AutoHeight clipping the answers. Scenario ask-batch-long-prompt-fits-viewport now uses six long options and asserts the answer field is reachable; it fails without the fix and passes with it.

{/* Kept at full height so the cap above scrolls it rather than clipping its end. */}
<AskUserQuestions
actionId={liveAsk.actionId}
questions={questions}
className="shrink-0"
headerAction={<MoveToChatButton onClick={collapse} />}
/>
</div>
</div>
);
}

/** Moves the question to the chat card and hands the composer back. */
function MoveToChatButton({ onClick }: { onClick: () => void }) {
const localize = useLocalize();
return (
<TooltipAnchor
description={localize('com_ui_ask_move_to_chat')}
side="top"
render={
<Button
variant="row-action"
size="icon-xs"
aria-label={localize('com_ui_ask_move_to_chat')}
onClick={onClick}
>
<ChevronDown
className="size-4 [view-transition-name:ask-question-chevron]"
aria-hidden="true"
/>
</Button>
}
/>
);
}

/**
* Split from the gate above so the per-keystroke `useWatch` subscription only
* exists while the popover is actually visible — the invisible popover was
Expand Down Expand Up @@ -149,18 +157,18 @@ function AskUserQuestionPopoverPanel({
const composerHasText = composerText.trim().length > 0;

return (
<div className="absolute bottom-28 z-10 w-full space-y-2">
<div className="absolute bottom-full z-10 mb-2 w-full">
{/* Digit shortcuts (1..N) work when focus is inside the popover too, not
only from the composer — keydown bubbles here from the focused row/
control. Height is viewport-bounded with the option list as the only
scroll region: the panel is absolutely positioned, so anything that
overflows it is unreachable by page scroll. */}
<div
className="popover border-border-light bg-surface-primary-alt flex max-h-[60vh] flex-col rounded-2xl border p-2 shadow-lg [view-transition-name:ask-question]"
className="popover border-border-light bg-surface-secondary flex max-h-[60vh] flex-col rounded-2xl border p-2 shadow-lg [view-transition-name:ask-question]"
onKeyDown={handlePopoverKeyDown}
>
<div className="flex shrink-0 items-start justify-between gap-2 p-2">
<div className="max-h-[24vh] min-w-0 overflow-y-auto">
<div className="text-text-secondary flex shrink-0 items-start justify-between gap-2 p-1 pl-2">
<div className="max-h-[24vh] min-w-0 overflow-y-auto pt-1">
<p className="text-text-primary text-sm font-medium [overflow-wrap:anywhere]">
{liveAsk.question.question}
</p>
Expand All @@ -170,23 +178,7 @@ function AskUserQuestionPopoverPanel({
</p>
)}
</div>
{/* Single exit: moves the question to the chat card and hands the
composer back for normal messages. */}
<TooltipAnchor
description={localize('com_ui_ask_move_to_chat')}
side="top"
render={
<Button
variant="ghost"
size="icon"
aria-label={localize('com_ui_ask_move_to_chat')}
className="text-text-secondary size-auto rounded-md p-1"
onClick={collapse}
>
<ChevronDown className="size-4" aria-hidden="true" />
</Button>
}
/>
<MoveToChatButton onClick={collapse} />
</div>
<AskOptions
options={options}
Expand Down
7 changes: 5 additions & 2 deletions client/src/components/Chat/Input/ChatForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,9 @@ const ChatForm = memo(function ChatForm({
textAreaRef,
submitButtonRef,
setIsScrollable,
disabled: disableInputs || answerMode.composerLocked,
/* Only picks the missing-key placeholder; a batch lock is applied to the
textarea itself and keeps its own "answer above" placeholder. */
disabled: disableInputs,
// The composer IS the free-form answer box while a question pause is live.
placeholder: composerReserved ? answerPlaceholder : placeholder,
// Enter stays live during a run when it can steer/queue instead of send.
Expand Down Expand Up @@ -864,6 +866,8 @@ const ChatForm = memo(function ChatForm({
'relative flex-1',
listening &&
'[&_textarea]:caret-transparent [&_textarea]:placeholder:text-transparent',
/* Locked behind a question: the placeholder is a notice, not text. */
answerMode.composerLocked && '[&_textarea]:select-none',
)}
style={
isCollapsed
Expand Down Expand Up @@ -1017,7 +1021,6 @@ const ChatForm = memo(function ChatForm({
isSubmitting={isSubmitting}
duringRunActive={steering.duringRunActive}
canControlGeneration={steering.canControlGeneration}
canStop={canStop}
steerInterruptsByDefault={steering.steerInterruptsByDefault}
duringRunAction={steering.effectiveAction}
/* A staged reasoning choice forces the message to queue, and the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ jest.mock('~/hooks/Input/useComposerBindings', () => ({
}),
}));

jest.mock('~/hooks/useKeyboardShortcuts', () => ({
useShortcutDisplay: () => 'Ctrl+Shift+X',
}));

const baseState: ComposerHintState = {
hasText: true,
isSubmitting: false,
Expand Down
104 changes: 85 additions & 19 deletions client/src/components/Chat/Messages/Content/AskUserQuestion.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useContext, useMemo, useState } from 'react';
import { Button, TextareaAutosize, TooltipAnchor } from '@librechat/client';
import { ChevronUp, MessageCircleQuestion, TriangleAlert } from 'lucide-react';
import { ChevronDown, MessageCircleQuestion, TriangleAlert } from 'lucide-react';
import type { Agents } from 'librechat-data-provider';
import { useApprovalContext, useAskSubmitStatus, useResumeSubmit } from './ApprovalContext';
import { splitOtherOption, ASK_USER_DECLINED_ANSWER } from '~/utils/approval';
Expand Down Expand Up @@ -33,21 +33,80 @@ export default function AskUserQuestion({
const answerMode = useAskAnswerMode(conversationId);
const isLivePause = answerMode.liveAsk?.actionId === actionId;
if (questions != null && questions.length > 0) {
if (answerMode.popoverVisible && isLivePause) {
return null;
}
return (
<AskUserQuestions
/** Same footprint reservation as a single question: while the popover owns
* the batch, a hidden copy of the card holds its place in the thread, so
* moving it between the composer and the chat reflows nothing. */
const reserved = answerMode.popoverVisible && isLivePause;
const card = (
<AskUserQuestionsCard
actionId={actionId}
questions={questions}
className="border-border-light bg-surface-secondary my-2 max-h-[70vh] w-full rounded-lg border"
live={isLivePause && !reserved}
reserved={reserved}
onExpand={answerMode.collapsed && isLivePause ? answerMode.expand : undefined}
/>
);
return reserved ? <AskingPlaceholder>{card}</AskingPlaceholder> : card;
}
return <AskUserQuestionSingle actionId={actionId} question={question} answerMode={answerMode} />;
}

/** The batch's chat card. Shares the popover's view-transition-name while it is
* the live pause's surface, so moving the batch between the composer and the
* chat morphs one surface into the other, like a single question does. */
function AskUserQuestionsCard({
actionId,
questions,
live,
reserved,
onExpand,
}: {
actionId: string;
questions: Agents.AskUserQuestionBatchItem[];
live: boolean;
reserved: boolean;
onExpand?: () => void;
}) {
const localize = useLocalize();
return (
<div
className={cn(
'border-border-light bg-surface-secondary my-2 flex w-full flex-col rounded-2xl border',
live && '[view-transition-name:ask-question]',
reserved && 'invisible',
)}
aria-hidden={reserved || undefined}
inert={reserved ? '' : undefined}
>
<AskUserQuestions
actionId={actionId}
questions={questions}
headerAction={
onExpand != null && (
<TooltipAnchor
description={localize('com_ui_ask_move_to_composer')}
side="top"
render={
<Button
variant="row-action"
size="icon-xs"
aria-label={localize('com_ui_ask_move_to_composer')}
onClick={onExpand}
>
<ChevronDown
className="size-4 rotate-180 [view-transition-name:ask-question-chevron]"
aria-hidden="true"
/>
</Button>
}
/>
)
}
/>
</div>
);
}

function AskUserQuestionSingle({
actionId,
question,
Expand Down Expand Up @@ -169,14 +228,14 @@ function AskUserQuestionSingle({
const card = (
<div
className={cn(
'border-border-light bg-surface-secondary my-2 flex w-full flex-col gap-2.5 rounded-xl border p-3',
'border-border-light bg-surface-secondary my-2 flex w-full flex-col gap-2.5 rounded-2xl border p-3',
showPlaceholder && 'invisible',
isLivePause && !showPlaceholder && '[view-transition-name:ask-question]',
)}
aria-hidden={showPlaceholder || undefined}
style={isLivePause && !showPlaceholder ? { viewTransitionName: 'ask-question' } : undefined}
>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<div className="text-text-secondary flex items-start justify-between gap-2">
<div className="min-w-0 pt-1">
<p className="text-text-primary text-sm font-medium [overflow-wrap:anywhere]">
{question.question}
</p>
Expand All @@ -192,13 +251,15 @@ function AskUserQuestionSingle({
side="top"
render={
<Button
variant="ghost"
size="icon"
variant="row-action"
size="icon-xs"
aria-label={localize('com_ui_ask_move_to_composer')}
className="text-text-secondary size-auto rounded-md p-1"
onClick={expand}
>
<ChevronUp className="size-4" aria-hidden="true" />
<ChevronDown
className="size-4 rotate-180 [view-transition-name:ask-question-chevron]"
aria-hidden="true"
/>
</Button>
}
/>
Expand Down Expand Up @@ -257,12 +318,17 @@ function AskUserQuestionSingle({
return card;
}

/** Popover has the question: the reserved card sits hidden underneath the
* same compact in-progress row the other tools use, so the turn still
* shows the call is running. */
return <AskingPlaceholder>{card}</AskingPlaceholder>;
}

/** Popover has the question: the reserved card sits hidden underneath the
* same compact in-progress row the other tools use, so the turn still
* shows the call is running. */
function AskingPlaceholder({ children }: { children: React.ReactNode }) {
const localize = useLocalize();
return (
<div className="relative">
{card}
{children}
<div className="absolute inset-x-0 top-0 my-1 flex h-5 items-center gap-2.5">
<MessageCircleQuestion className="text-text-secondary size-4 shrink-0" aria-hidden="true" />
<span className="tool-status-text shimmer text-text-secondary font-medium">
Expand Down
Loading
Loading