import { createStore } from "solid-js/store" import { createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js" import { useRenderer } from "@opentui/solid" import type { TextareaRenderable } from "@opentui/core" import { selectedForeground, tint, useTheme } from "../../context/theme" import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2" import { useSDK } from "../../context/sdk" import { SplitBorder } from "../../ui/border" import { useTuiConfig } from "../../config" import { useBindings, useOpencodeModeStack } from "../../keymap" const QUESTION_MODE = "question" export function QuestionPrompt(props: { request: QuestionRequest; directory?: string }) { const sdk = useSDK() const { theme } = useTheme() const renderer = useRenderer() const tuiConfig = useTuiConfig() const modeStack = useOpencodeModeStack() 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 [tabHover, setTabHover] = createSignal(null) 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 custom = createMemo(() => question()?.custom !== false) const other = createMemo(() => custom() && 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] ?? []) void sdk.client.question.reply({ requestID: props.request.id, directory: props.directory, answers, }) } function reject() { void sdk.client.question.reject({ requestID: props.request.id, directory: props.directory, }) } 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()) { void sdk.client.question.reply({ requestID: props.request.id, directory: props.directory, 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) } function moveTo(index: number) { setStore("selected", index) } function selectTab(index: number) { setStore("tab", index) setStore("selected", 0) } function selectOption() { if (other()) { if (!multi()) { setStore("editing", true) return } const value = input() if (value && customPicked()) { toggle(value) return } setStore("editing", true) return } const opt = options()[store.selected] if (!opt) return if (multi()) { toggle(opt.label) return } pick(opt.label) } onMount(() => { const popMode = modeStack.push(QUESTION_MODE) onCleanup(popMode) }) useBindings(() => ({ mode: QUESTION_MODE, enabled: store.editing && !confirm(), commands: [ { name: "prompt.clear", title: "Clear answer edit", category: "Question", run() { const text = textarea?.plainText ?? "" if (!text) { setStore("editing", false) return } textarea?.setText("") }, }, ], bindings: [ { key: "escape", desc: "Cancel answer edit", group: "Question", cmd: () => { setStore("editing", false) }, }, ...tuiConfig.keybinds.get("prompt.clear"), { key: "return", desc: "Submit answer edit", group: "Question", cmd: () => { 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] answers[store.tab] = (answers[store.tab] ?? []).filter((x) => x !== prev) 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) }, }, ], })) useBindings(() => { const opts = options() const total = opts.length + (custom() ? 1 : 0) const max = Math.min(total, 9) return { mode: QUESTION_MODE, enabled: !store.editing, commands: [ { name: "app.exit", title: "Reject question", category: "Question", run() { reject() }, }, ], bindings: [ { key: "left", desc: "Previous question", group: "Question", cmd: () => selectTab((store.tab - 1 + tabs()) % tabs()), }, { key: "h", desc: "Previous question", group: "Question", cmd: () => selectTab((store.tab - 1 + tabs()) % tabs()), }, { key: "right", desc: "Next question", group: "Question", cmd: () => selectTab((store.tab + 1) % tabs()) }, { key: "l", desc: "Next question", group: "Question", cmd: () => selectTab((store.tab + 1) % tabs()) }, { key: "tab", desc: "Next question", group: "Question", cmd: ({ event }: { event: { shift: boolean } }) => { selectTab((store.tab + (event.shift ? -1 : 1) + tabs()) % tabs()) }, }, ...(confirm() ? [ { key: "return", desc: "Submit answer", group: "Question", cmd: () => submit() }, { key: "escape", desc: "Reject question", group: "Question", cmd: () => reject() }, ...tuiConfig.keybinds.get("app.exit"), ] : [ ...Array.from({ length: max }, (_, index) => ({ key: String(index + 1), desc: `Select answer ${index + 1}`, group: "Question", cmd: () => { moveTo(index) selectOption() }, })), { key: "up", desc: "Previous answer", group: "Question", cmd: () => moveTo((store.selected - 1 + total) % total), }, { key: "k", desc: "Previous answer", group: "Question", cmd: () => moveTo((store.selected - 1 + total) % total), }, { key: "down", desc: "Next answer", group: "Question", cmd: () => moveTo((store.selected + 1) % total) }, { key: "j", desc: "Next answer", group: "Question", cmd: () => moveTo((store.selected + 1) % total) }, { key: "return", desc: "Select answer", group: "Question", cmd: () => selectOption() }, { key: "escape", desc: "Reject question", group: "Question", cmd: () => reject() }, ...tuiConfig.keybinds.get("app.exit"), ]), ], } }) return ( {(q, index) => { const isActive = () => index() === store.tab const isAnswered = () => { return (store.answers[index()]?.length ?? 0) > 0 } return ( setTabHover(index())} onMouseOut={() => setTabHover(null)} onMouseUp={() => { if (renderer.getSelection()?.getSelectedText()) return selectTab(index()) }} > {q.header} ) }} setTabHover("confirm")} onMouseOut={() => setTabHover(null)} onMouseUp={() => { if (renderer.getSelection()?.getSelectedText()) return selectTab(questions().length) }} > 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 ( moveTo(i())} onMouseDown={() => moveTo(i())} onMouseUp={() => { if (renderer.getSelection()?.getSelectedText()) return selectOption() }} > {`${i() + 1}.`} {multi() ? `[${picked() ? "✓" : " "}] ${opt.label}` : opt.label} {picked() ? " ✓" : ""} {opt.description} ) }} moveTo(options().length)} onMouseDown={() => moveTo(options().length)} onMouseUp={() => { if (renderer.getSelection()?.getSelectedText()) return selectOption() }} > {`${options().length + 1}.`} {multi() ? `[${customPicked() ? "✓" : " "}] Type your own answer` : "Type your own answer"} {customPicked() ? " ✓" : ""}