import { createStore } from "solid-js/store" import { createMemo, For, Show } from "solid-js" import { useKeyboard } from "@opentui/solid" import type { TextareaRenderable } from "@opentui/core" import { useKeybind } from "../../context/keybind" import { useTheme } from "../../context/theme" import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2" import { useSDK } from "../../context/sdk" import { SplitBorder } from "../../component/border" import { useTextareaKeybindings } from "../../component/textarea-keybindings" import { useDialog } from "../../ui/dialog" export function QuestionPrompt(props: { request: QuestionRequest }) { const sdk = useSDK() const { theme } = useTheme() const keybind = useKeybind() const bindings = useTextareaKeybindings() const questions = createMemo(() => props.request.questions) const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true) const tabs = createMemo(() => (single() ? 1 : questions().length + 1)) // questions + confirm tab (no confirm for single select) const [store, setStore] = createStore({ tab: 0, answers: [] as QuestionAnswer[], custom: [] as string[], selected: 0, editing: false, }) let textarea: TextareaRenderable | undefined const question = createMemo(() => questions()[store.tab]) const confirm = createMemo(() => !single() && store.tab === questions().length) const options = createMemo(() => question()?.options ?? []) const other = createMemo(() => store.selected === options().length) const input = createMemo(() => store.custom[store.tab] ?? "") const multi = createMemo(() => question()?.multiple === true) const customPicked = createMemo(() => { const value = input() if (!value) return false return store.answers[store.tab]?.includes(value) ?? false }) function submit() { const answers = questions().map((_, i) => store.answers[i] ?? []) sdk.client.question.reply({ requestID: props.request.id, answers, }) } function reject() { sdk.client.question.reject({ requestID: props.request.id, }) } function pick(answer: string, custom: boolean = false) { const answers = [...store.answers] answers[store.tab] = [answer] setStore("answers", answers) if (custom) { const inputs = [...store.custom] inputs[store.tab] = answer setStore("custom", inputs) } if (single()) { sdk.client.question.reply({ requestID: props.request.id, answers: [[answer]], }) return } setStore("tab", store.tab + 1) setStore("selected", 0) } function toggle(answer: string) { const existing = store.answers[store.tab] ?? [] const next = [...existing] const index = next.indexOf(answer) if (index === -1) next.push(answer) if (index !== -1) next.splice(index, 1) const answers = [...store.answers] answers[store.tab] = next setStore("answers", answers) } const dialog = useDialog() useKeyboard((evt) => { // When editing "Other" textarea if (store.editing && !confirm()) { if (evt.name === "escape") { evt.preventDefault() setStore("editing", false) return } if (evt.name === "return") { evt.preventDefault() const text = textarea?.plainText?.trim() ?? "" const prev = store.custom[store.tab] if (!text) { if (prev) { const inputs = [...store.custom] inputs[store.tab] = "" setStore("custom", inputs) } const answers = [...store.answers] if (prev) { answers[store.tab] = (answers[store.tab] ?? []).filter((x) => x !== prev) } if (!prev) { answers[store.tab] = [] } setStore("answers", answers) setStore("editing", false) return } if (multi()) { const inputs = [...store.custom] inputs[store.tab] = text setStore("custom", inputs) const existing = store.answers[store.tab] ?? [] const next = [...existing] if (prev) { const index = next.indexOf(prev) if (index !== -1) next.splice(index, 1) } if (!next.includes(text)) next.push(text) const answers = [...store.answers] answers[store.tab] = next setStore("answers", answers) setStore("editing", false) return } pick(text, true) setStore("editing", false) return } // Let textarea handle all other keys return } if (evt.name === "left" || evt.name === "h") { evt.preventDefault() const next = (store.tab - 1 + tabs()) % tabs() setStore("tab", next) setStore("selected", 0) } if (evt.name === "right" || evt.name === "l") { evt.preventDefault() const next = (store.tab + 1) % tabs() setStore("tab", next) setStore("selected", 0) } if (confirm()) { if (evt.name === "return") { evt.preventDefault() submit() } if (evt.name === "escape" || keybind.match("app_exit", evt)) { evt.preventDefault() reject() } } else { const opts = options() const total = opts.length + 1 // options + "Other" if (evt.name === "up" || evt.name === "k") { evt.preventDefault() setStore("selected", (store.selected - 1 + total) % total) } if (evt.name === "down" || evt.name === "j") { evt.preventDefault() setStore("selected", (store.selected + 1) % total) } if (evt.name === "return") { evt.preventDefault() if (other()) { if (!multi()) { setStore("editing", true) return } const value = input() if (value && customPicked()) { toggle(value) return } setStore("editing", true) return } const opt = opts[store.selected] if (!opt) return if (multi()) { toggle(opt.label) return } pick(opt.label) } if (evt.name === "escape" || keybind.match("app_exit", evt)) { evt.preventDefault() reject() } } }) return ( {(q, index) => { const isActive = () => index() === store.tab const isAnswered = () => { return (store.answers[index()]?.length ?? 0) > 0 } return ( {q.header} ) }} Confirm {question()?.question} {multi() ? " (select all that apply)" : ""} {(opt, i) => { const active = () => i() === store.selected const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false return ( {i() + 1}. {opt.label} {picked() ? "✓" : ""} {opt.description} ) }} {options().length + 1}. Type your own answer {customPicked() ? "✓" : ""}