import { createMemo, Show, For, createEffect, type Accessor } from "solid-js" import type { ToolState } from "@opencode-ai/sdk" import type { QuestionRequest } from "@opencode-ai/sdk/v2" import { useI18n } from "../../lib/i18n" type QuestionOption = { label: string; description: string } type QuestionPrompt = { header: string question: string options: QuestionOption[] multiple?: boolean } export type QuestionToolBlockProps = { toolName: Accessor toolState: Accessor toolCallId: Accessor request: Accessor active: Accessor submitting: Accessor error: Accessor draftAnswers: Accessor> setDraftAnswers: (updater: (prev: Record) => Record) => void onSubmit: () => void | Promise onDismiss: () => void | Promise } export function QuestionToolBlock(props: QuestionToolBlockProps) { const { t } = useI18n() let firstInputRef: HTMLInputElement | undefined createEffect(() => { if (props.active() && firstInputRef) { firstInputRef.focus() } }) const requestId = createMemo(() => { const state = props.toolState() const request = props.request() return request?.id ?? (state as any)?.input?.requestID ?? `question-${props.toolCallId()}` }) const questions = createMemo(() => { const state = props.toolState() const request = props.request() const isQuestionTool = props.toolName() === "question" if (!request && !isQuestionTool) return [] as QuestionPrompt[] const questionsSource = request?.questions ?? ((state as any)?.input?.questions as any[] | undefined) ?? [] const list = Array.isArray(questionsSource) ? questionsSource : [] return list as QuestionPrompt[] }) const isVisible = createMemo(() => { const request = props.request() const isQuestionTool = props.toolName() === "question" return Boolean(request) || isQuestionTool }) const answers = createMemo(() => { const state = props.toolState() const completedAnswers = (state as any)?.status === "completed" && Array.isArray((state as any)?.metadata?.answers) ? ((state as any).metadata.answers as string[][]) : undefined if (completedAnswers) return completedAnswers const request = props.request() const requestAnswers = request?.questions?.map((q) => (q as any)?.answer) // defensive (if server ever inlines) if (Array.isArray(requestAnswers) && requestAnswers.some((row) => Array.isArray(row) && row.length > 0)) { return requestAnswers as string[][] } const draft = props.draftAnswers()[requestId()] ?? [] return Array.isArray(draft) ? draft : [] }) const hasFinalAnswers = createMemo(() => { const state = props.toolState() if ((state as any)?.status === "completed") return true const request = props.request() const requestAnswers = request?.questions?.map((q) => (q as any)?.answer) if (Array.isArray(requestAnswers) && requestAnswers.length > 0) { return requestAnswers.every((row) => Array.isArray(row) && row.length > 0) } return false }) const updateAnswer = (questionIndex: number, next: string[]) => { if (!props.active()) return props.setDraftAnswers((prev) => { const current = prev[requestId()] ?? [] const updated = [...current] updated[questionIndex] = next return { ...prev, [requestId()]: updated } }) } const toggleOption = (questionIndex: number, label: string) => { const info = questions()[questionIndex] const multi = info?.multiple === true const existing = answers()[questionIndex] ?? [] if (multi) { const next = existing.includes(label) ? existing.filter((x) => x !== label) : [...existing, label] updateAnswer(questionIndex, next) return } updateAnswer(questionIndex, [label]) } const submitDisabled = () => { if (!props.active()) return true if (props.submitting()) return true return questions().some((_, index) => (answers()[index]?.length ?? 0) === 0) } const toggleFromCustomInput = (questionIndex: number, input: HTMLInputElement | null) => { if (!props.active()) return const rawValue = input?.value ?? "" const value = rawValue if (value.trim().length === 0) return const info = questions()[questionIndex] const multi = info?.multiple === true if (!multi) { // When switching a radio to custom, clear existing selection first. updateAnswer(questionIndex, []) toggleOption(questionIndex, value) return } // For multi-select, focusing the input should never toggle an existing custom value off. // Ensure the current input value is selected; removal is handled by unchecking Custom. const existing = answers()[questionIndex] ?? [] if (!existing.includes(value)) { updateAnswer(questionIndex, [...existing, value]) } } const clearCustomAnswer = (questionIndex: number, valuesToRemove: string[]) => { if (!props.active()) return if (valuesToRemove.length === 0) return const existing = answers()[questionIndex] ?? [] const next = existing.filter((value) => !valuesToRemove.includes(value)) updateAnswer(questionIndex, next) } const handleCustomTyping = (questionIndex: number, input: HTMLInputElement) => { if (!props.active()) return const value = input.value const trimmed = value.trim() const info = questions()[questionIndex] const multi = info?.multiple === true if (!multi) { updateAnswer(questionIndex, trimmed.length > 0 ? [value] : []) return } const optionLabels = new Set((info?.options ?? []).map((opt) => opt.label)) const existing = answers()[questionIndex] ?? [] const last = input.dataset.lastValue ?? "" let next = existing.filter((item) => item !== last) if (trimmed.length > 0) { // Only treat it as custom if it doesn't match an existing option label. if (!optionLabels.has(trimmed) && !next.includes(value)) { next = [...next, value] } else if (optionLabels.has(trimmed)) { // If they typed an existing option label, don't treat it as custom. } else if (!next.includes(value)) { next = [...next, value] } input.dataset.lastValue = value } else { delete input.dataset.lastValue } updateAnswer(questionIndex, next) } return ( 0}>
{(q, index) => { const i = () => index() const multi = () => q?.multiple === true const selected = () => answers()[i()] ?? [] const inputType = () => (multi() ? "checkbox" : "radio") const groupName = () => `question-${requestId()}-${i()}` const optionLabels = () => new Set((q?.options ?? []).map((opt) => opt.label)) const customSelected = () => selected().filter((value) => !optionLabels().has(value)) const customValue = () => customSelected()[0] ?? "" const customChecked = () => customValue().length > 0 return (
{t("toolCall.question.number", { number: i() + 1 })} {q?.header}
{t("toolCall.question.multiple")}
{q?.question}
{(opt, optIndex) => { const checked = () => selected().includes(opt.label) return ( ) }}
) }}
Enter {t("toolCall.question.shortcuts.submit")} Esc {t("toolCall.question.shortcuts.dismiss")}
{props.error()}

{t("toolCall.question.queuedText")}

) }