diff --git a/client/src/components/Chat/Messages/Content/AskUserQuestionCall.tsx b/client/src/components/Chat/Messages/Content/AskUserQuestionCall.tsx index a3d622ab9c..57675883ee 100644 --- a/client/src/components/Chat/Messages/Content/AskUserQuestionCall.tsx +++ b/client/src/components/Chat/Messages/Content/AskUserQuestionCall.tsx @@ -1,3 +1,5 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useRecoilValue } from 'recoil'; import { MessageCircleQuestion, TriangleAlert } from 'lucide-react'; import type { Agents } from 'librechat-data-provider'; import { @@ -6,9 +8,11 @@ import { parseAskUserQuestionsArgs, } from '~/utils/approval'; import AskUserQuestionProgress from './AskUserQuestionProgress'; +import { useLocalize, useExpandCollapse } from '~/hooks'; +import ProgressText from './ProgressText'; import EmptyText from './Parts/EmptyText'; -import { useLocalize } from '~/hooks'; import Container from './Container'; +import store from '~/store'; /** * Static rendering of a COMPLETED (or abandoned) `ask_user_question` tool call — @@ -16,6 +20,12 @@ import Container from './Container'; * is wrong here: it labels a no-output call "cancelled" and shows raw JSON args. * The interactive card ({@link AskUserQuestion}) renders only while the pause is * live; this component owns the part everywhere else (history, reload, exports). + * + * Settled, it is history — so it reads as one collapsed tool-call line (status + * label plus the question itself) and opens on demand, under the same + * `autoExpandTools` preference every other tool card follows. Answers are + * frequently long, multi-paragraph text; left expanded they buried the reply + * that followed them. */ export default function AskUserQuestionCall({ args, @@ -24,6 +34,7 @@ export default function AskUserQuestionCall({ isSubmitting = false, failed = false, showCursor = false, + onExpand, }: { args: string | Record | undefined; output: string; @@ -31,8 +42,29 @@ export default function AskUserQuestionCall({ isSubmitting?: boolean; failed?: boolean; showCursor?: boolean; + onExpand?: () => void; }) { const localize = useLocalize(); + const autoExpand = useRecoilValue(store.autoExpandTools); + const [expanded, setExpanded] = useState(autoExpand); + const { style: expandStyle, ref: expandRef } = useExpandCollapse(expanded); + + useEffect(() => { + if (autoExpand) { + setExpanded(true); + } + }, [autoExpand]); + + const toggleExpanded = useCallback(() => { + setExpanded((prev) => { + const next = !prev; + if (next) { + onExpand?.(); + } + return next; + }); + }, [onExpand]); + const question = parseAskUserQuestionArgs(args); const batch = parseAskUserQuestionsArgs(args); /** @@ -88,137 +120,169 @@ export default function AskUserQuestionCall({ ) : null; - if (batch != null) { - let statusLabel = localize('com_ui_asking'); + const count = batch?.questions.length ?? 1; + /** + * Past tense unconditionally: a live, unanswered pause returns above, so + * every state that reaches this header is settled — answered, abandoned + * (the run stopped before an answer), or rejected. An abandoned pause was + * still ASKED; it explains itself with "no answer" inside the panel, and a + * present-tense summary would strand it as permanently in-flight now that + * the panel starts closed. Matches `ToolCallGroup`, which settles its own + * question header on `!isSubmitting`. + */ + const statusLabel = (() => { if (failed) { - statusLabel = localize('com_ui_question_failed'); - } else if (answered) { - statusLabel = localize('com_ui_asked'); + return localize('com_ui_question_failed'); } - return ( - <> -
-
- {failed ? ( -