import { useMemo, useRef, useState } from 'react' import { useChatStore } from '../../stores/chatStore' import { useTabStore } from '../../stores/tabStore' import { useTranslation } from '../../i18n' import { Button } from '../shared/Button' type QuestionOption = { label: string description?: string } type Question = { question: string header?: string options?: QuestionOption[] multiSelect?: boolean } type AskUserInput = { questions?: Question[] question?: string header?: string options?: QuestionOption[] multiSelect?: boolean } type Props = { sessionId?: string | null toolUseId: string input: unknown result?: unknown } /** * Parse the AskUserQuestion input which may come in different shapes. */ function parseInput(input: unknown): Question[] { if (!input || typeof input !== 'object') return [] const obj = input as AskUserInput // Shape 1: { questions: [...] } if (Array.isArray(obj.questions)) { return obj.questions } // Shape 2: { question: "...", options: [...] } if (typeof obj.question === 'string') { return [{ question: obj.question, header: obj.header, options: obj.options, multiSelect: obj.multiSelect, }] } return [] } type QuestionSelections = Record type QuestionFreeTexts = Record function getSelectedAnswer(question: Question, selected: string[] | undefined) { if (!selected || selected.length === 0) return '' return question.multiSelect ? selected.join(', ') : selected[0] ?? '' } export function AskUserQuestion({ sessionId, toolUseId, input, result }: Props) { const { respondToPermission } = useChatStore() const activeTabId = useTabStore((s) => s.activeTabId) const targetSessionId = sessionId ?? activeTabId const pendingPermission = useChatStore((s) => targetSessionId ? s.sessions[targetSessionId]?.pendingPermission : undefined) const t = useTranslation() const questions = parseInput(input) const inputObject = (input && typeof input === 'object') ? input as Record : {} const [activeTab, setActiveTab] = useState(0) const [selections, setSelections] = useState({}) const [freeTexts, setFreeTexts] = useState({}) const [hasSubmitted, setHasSubmitted] = useState(false) const composingRef = useRef(false) if (questions.length === 0) return null const safeActiveTab = Math.min(activeTab, questions.length - 1) const activeQuestion = questions[safeActiveTab] const resultAnswers = useMemo(() => { if (!result || typeof result !== 'object') return {} const answers = (result as { answers?: unknown }).answers return answers && typeof answers === 'object' ? answers as Record : {} }, [result]) const resultText = typeof result === 'string' && result.trim().length > 0 ? result.trim() : '' const hasStructuredAnswers = Object.keys(resultAnswers).length > 0 const hasTerminalResult = hasStructuredAnswers || resultText.length > 0 const pendingRequest = pendingPermission?.toolUseId === toolUseId ? pendingPermission : null const answeredText = useMemo(() => { if (hasStructuredAnswers) { return questions .map((question) => resultAnswers[question.question]) .filter((answer): answer is string => typeof answer === 'string' && answer.trim().length > 0) .join(', ') } if (resultText) return resultText return questions .map((question, index) => freeTexts[index]?.trim() || getSelectedAnswer(question, selections[index])) .filter(Boolean) .join('; ') }, [freeTexts, hasStructuredAnswers, questions, resultAnswers, resultText, selections]) const submitted = hasTerminalResult || hasSubmitted const terminalWithoutAnswers = submitted && !hasStructuredAnswers && resultText.length > 0 const handleSelect = (qIndex: number, label: string) => { if (submitted) return setSelections((prev) => { const question = questions[qIndex] const selected = prev[qIndex] ?? [] if (question?.multiSelect) { const nextSelected = selected.includes(label) ? selected.filter((value) => value !== label) : [...selected, label] const next = { ...prev } if (nextSelected.length > 0) { next[qIndex] = nextSelected } else { delete next[qIndex] } return next } if (selected[0] === label) { const next = { ...prev } delete next[qIndex] return next } return { ...prev, [qIndex]: [label] } }) setFreeTexts((prev) => { if (!prev[qIndex]) return prev const next = { ...prev } delete next[qIndex] return next }) } const handleFreeTextChange = (qIndex: number, value: string) => { if (submitted) return setFreeTexts((prev) => { const next = { ...prev } if (value) { next[qIndex] = value } else { delete next[qIndex] } return next }) if (value.trim()) { setSelections((prev) => { if (!prev[qIndex]) return prev const next = { ...prev } delete next[qIndex] return next }) } } const handleSubmit = () => { if (submitted) return const parts: string[] = [] for (let i = 0; i < questions.length; i++) { const answer = freeTexts[i]?.trim() || getSelectedAnswer(questions[i]!, selections[i]) if (answer) parts.push(answer) } const response = parts.join('; ') if (!response) return if (!targetSessionId || !pendingRequest) return const answers = questions.reduce>((acc, question, index) => { const freeText = freeTexts[index]?.trim() if (freeText) { acc[question.question] = freeText } else { const selected = getSelectedAnswer(question, selections[index]) if (selected) acc[question.question] = selected } return acc }, {}) setHasSubmitted(true) respondToPermission(targetSessionId, pendingRequest.requestId, true, { updatedInput: { ...inputObject, answers, }, }) } // All questions must be answered (via selection or free text) to enable submit const allAnswered = questions.every((_, i) => Boolean(freeTexts[i]?.trim()) || (selections[i]?.length ?? 0) > 0, ) if (!activeQuestion) return null return (
{/* Header */}
help
{t('question.needsInput')} {submitted && ( {t(terminalWithoutAnswers ? 'question.completed' : 'question.answered')} )}
{/* Question tabs — horizontal tab bar (only show when multiple questions) */} {questions.length > 1 && (
{questions.map((q, i) => { const isActive = safeActiveTab === i const isAnswered = Boolean(freeTexts[i]?.trim()) || (selections[i]?.length ?? 0) > 0 const tabLabel = q.header || `Q${i + 1}` return ( ) })}
)} {/* Active question content */}

{activeQuestion.question}

{/* Option cards */} {activeQuestion.options && activeQuestion.options.length > 0 && (
{activeQuestion.options.map((opt, optIndex) => { const isSelected = selections[safeActiveTab]?.includes(opt.label) ?? false const isMultiSelect = activeQuestion.multiSelect === true return ( ) })}
)} {/* Free text input */} {!submitted && (