@@ -4,7 +4,7 @@ import { useKeyboard } from "@opentui/solid"
44import type { TextareaRenderable } from "@opentui/core"
55import { useKeybind } from "../../context/keybind"
66import { useTheme } from "../../context/theme"
7- import type { QuestionRequest } from "@opencode-ai/sdk/v2"
7+ import type { QuestionAnswer , QuestionRequest } from "@opencode-ai/sdk/v2"
88import { useSDK } from "../../context/sdk"
99import { SplitBorder } from "../../component/border"
1010import { useTextareaKeybindings } from "../../component/textarea-keybindings"
@@ -17,11 +17,11 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
1717 const bindings = useTextareaKeybindings ( )
1818
1919 const questions = createMemo ( ( ) => props . request . questions )
20- const single = createMemo ( ( ) => questions ( ) . length === 1 )
21- const tabs = createMemo ( ( ) => ( single ( ) ? 1 : questions ( ) . length + 1 ) ) // questions + confirm tab (no confirm for single)
20+ const single = createMemo ( ( ) => questions ( ) . length === 1 && questions ( ) [ 0 ] ?. multiple !== true )
21+ const tabs = createMemo ( ( ) => ( single ( ) ? 1 : questions ( ) . length + 1 ) ) // questions + confirm tab (no confirm for single select )
2222 const [ store , setStore ] = createStore ( {
2323 tab : 0 ,
24- answers : [ ] as string [ ] ,
24+ answers : [ ] as QuestionAnswer [ ] ,
2525 custom : [ ] as string [ ] ,
2626 selected : 0 ,
2727 editing : false ,
@@ -34,10 +34,15 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
3434 const options = createMemo ( ( ) => question ( ) ?. options ?? [ ] )
3535 const other = createMemo ( ( ) => store . selected === options ( ) . length )
3636 const input = createMemo ( ( ) => store . custom [ store . tab ] ?? "" )
37+ const multi = createMemo ( ( ) => question ( ) ?. multiple === true )
38+ const customPicked = createMemo ( ( ) => {
39+ const value = input ( )
40+ if ( ! value ) return false
41+ return store . answers [ store . tab ] ?. includes ( value ) ?? false
42+ } )
3743
3844 function submit ( ) {
39- // Fill in empty answers with empty strings
40- const answers = questions ( ) . map ( ( _ , i ) => store . answers [ i ] ?? "" )
45+ const answers = questions ( ) . map ( ( _ , i ) => store . answers [ i ] ?? [ ] )
4146 sdk . client . question . reply ( {
4247 requestID : props . request . id ,
4348 answers,
@@ -52,7 +57,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
5257
5358 function pick ( answer : string , custom : boolean = false ) {
5459 const answers = [ ...store . answers ]
55- answers [ store . tab ] = answer
60+ answers [ store . tab ] = [ answer ]
5661 setStore ( "answers" , answers )
5762 if ( custom ) {
5863 const inputs = [ ...store . custom ]
@@ -62,14 +67,25 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
6267 if ( single ( ) ) {
6368 sdk . client . question . reply ( {
6469 requestID : props . request . id ,
65- answers : [ answer ] ,
70+ answers : [ [ answer ] ] ,
6671 } )
6772 return
6873 }
6974 setStore ( "tab" , store . tab + 1 )
7075 setStore ( "selected" , 0 )
7176 }
7277
78+ function toggle ( answer : string ) {
79+ const existing = store . answers [ store . tab ] ?? [ ]
80+ const next = [ ...existing ]
81+ const index = next . indexOf ( answer )
82+ if ( index === - 1 ) next . push ( answer )
83+ if ( index !== - 1 ) next . splice ( index , 1 )
84+ const answers = [ ...store . answers ]
85+ answers [ store . tab ] = next
86+ setStore ( "answers" , answers )
87+ }
88+
7389 const dialog = useDialog ( )
7490
7591 useKeyboard ( ( evt ) => {
@@ -82,11 +98,49 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
8298 }
8399 if ( evt . name === "return" ) {
84100 evt . preventDefault ( )
85- const text = textarea ?. plainText ?. trim ( )
86- if ( text ) {
87- pick ( text , true )
101+ const text = textarea ?. plainText ?. trim ( ) ?? ""
102+ const prev = store . custom [ store . tab ]
103+
104+ if ( ! text ) {
105+ if ( prev ) {
106+ const inputs = [ ...store . custom ]
107+ inputs [ store . tab ] = ""
108+ setStore ( "custom" , inputs )
109+ }
110+
111+ const answers = [ ...store . answers ]
112+ if ( prev ) {
113+ answers [ store . tab ] = ( answers [ store . tab ] ?? [ ] ) . filter ( ( x ) => x !== prev )
114+ }
115+ if ( ! prev ) {
116+ answers [ store . tab ] = [ ]
117+ }
118+ setStore ( "answers" , answers )
119+ setStore ( "editing" , false )
120+ return
121+ }
122+
123+ if ( multi ( ) ) {
124+ const inputs = [ ...store . custom ]
125+ inputs [ store . tab ] = text
126+ setStore ( "custom" , inputs )
127+
128+ const existing = store . answers [ store . tab ] ?? [ ]
129+ const next = [ ...existing ]
130+ if ( prev ) {
131+ const index = next . indexOf ( prev )
132+ if ( index !== - 1 ) next . splice ( index , 1 )
133+ }
134+ if ( ! next . includes ( text ) ) next . push ( text )
135+ const answers = [ ...store . answers ]
136+ answers [ store . tab ] = next
137+ setStore ( "answers" , answers )
88138 setStore ( "editing" , false )
139+ return
89140 }
141+
142+ pick ( text , true )
143+ setStore ( "editing" , false )
90144 return
91145 }
92146 // Let textarea handle all other keys
@@ -133,13 +187,25 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
133187 if ( evt . name === "return" ) {
134188 evt . preventDefault ( )
135189 if ( other ( ) ) {
136- setStore ( "editing" , true )
137- } else {
138- const opt = opts [ store . selected ]
139- if ( opt ) {
140- pick ( opt . label )
190+ if ( ! multi ( ) ) {
191+ setStore ( "editing" , true )
192+ return
193+ }
194+ const value = input ( )
195+ if ( value && customPicked ( ) ) {
196+ toggle ( value )
197+ return
141198 }
199+ setStore ( "editing" , true )
200+ return
142201 }
202+ const opt = opts [ store . selected ]
203+ if ( ! opt ) return
204+ if ( multi ( ) ) {
205+ toggle ( opt . label )
206+ return
207+ }
208+ pick ( opt . label )
143209 }
144210
145211 if ( evt . name === "escape" || keybind . match ( "app_exit" , evt ) ) {
@@ -162,7 +228,9 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
162228 < For each = { questions ( ) } >
163229 { ( q , index ) => {
164230 const isActive = ( ) => index ( ) === store . tab
165- const isAnswered = ( ) => store . answers [ index ( ) ] !== undefined
231+ const isAnswered = ( ) => {
232+ return ( store . answers [ index ( ) ] ?. length ?? 0 ) > 0
233+ }
166234 return (
167235 < box
168236 paddingLeft = { 1 }
@@ -185,13 +253,16 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
185253 < Show when = { ! confirm ( ) } >
186254 < box paddingLeft = { 1 } gap = { 1 } >
187255 < box >
188- < text fg = { theme . text } > { question ( ) ?. question } </ text >
256+ < text fg = { theme . text } >
257+ { question ( ) ?. question }
258+ { multi ( ) ? " (select all that apply)" : "" }
259+ </ text >
189260 </ box >
190261 < box >
191262 < For each = { options ( ) } >
192263 { ( opt , i ) => {
193264 const active = ( ) => i ( ) === store . selected
194- const picked = ( ) => store . answers [ store . tab ] === opt . label
265+ const picked = ( ) => store . answers [ store . tab ] ?. includes ( opt . label ) ?? false
195266 return (
196267 < box >
197268 < box flexDirection = "row" gap = { 1 } >
@@ -212,17 +283,18 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
212283 < box >
213284 < box flexDirection = "row" gap = { 1 } >
214285 < box backgroundColor = { other ( ) ? theme . backgroundElement : undefined } >
215- < text fg = { other ( ) ? theme . secondary : input ( ) ? theme . success : theme . text } >
286+ < text fg = { other ( ) ? theme . secondary : customPicked ( ) ? theme . success : theme . text } >
216287 { options ( ) . length + 1 } . Type your own answer
217288 </ text >
218289 </ box >
219- < text fg = { theme . success } > { input ( ) ? "✓" : "" } </ text >
290+ < text fg = { theme . success } > { customPicked ( ) ? "✓" : "" } </ text >
220291 </ box >
221292 < Show when = { store . editing } >
222293 < box paddingLeft = { 3 } >
223294 < textarea
224295 ref = { ( val : TextareaRenderable ) => ( textarea = val ) }
225296 focused
297+ initialValue = { input ( ) }
226298 placeholder = "Type your own answer"
227299 textColor = { theme . text }
228300 focusedTextColor = { theme . text }
@@ -247,11 +319,12 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
247319 </ box >
248320 < For each = { questions ( ) } >
249321 { ( q , index ) => {
250- const answer = ( ) => store . answers [ index ( ) ]
322+ const value = ( ) => store . answers [ index ( ) ] ?. join ( ", " ) ?? ""
323+ const answered = ( ) => Boolean ( value ( ) )
251324 return (
252325 < box flexDirection = "row" gap = { 1 } paddingLeft = { 1 } >
253326 < text fg = { theme . textMuted } > { q . header } :</ text >
254- < text fg = { answer ( ) ? theme . text : theme . error } > { answer ( ) ?? "(not answered)" } </ text >
327+ < text fg = { answered ( ) ? theme . text : theme . error } > { answered ( ) ? value ( ) : "(not answered)" } </ text >
255328 </ box >
256329 )
257330 } }
@@ -279,8 +352,12 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
279352 </ text >
280353 </ Show >
281354 < text fg = { theme . text } >
282- enter < span style = { { fg : theme . textMuted } } > { confirm ( ) ? "submit" : single ( ) ? "submit" : "confirm" } </ span >
355+ enter{ " " }
356+ < span style = { { fg : theme . textMuted } } >
357+ { confirm ( ) ? "submit" : multi ( ) ? "toggle" : single ( ) ? "submit" : "confirm" }
358+ </ span >
283359 </ text >
360+
284361 < text fg = { theme . text } >
285362 esc < span style = { { fg : theme . textMuted } } > dismiss</ span >
286363 </ text >
0 commit comments