Skip to content

Commit 8cf1572

Browse files
committed
feat(tui): add markdown support for question display
- Replace plain text rendering with syntax-highlighted code component for questions - Add scrollable content area with height constraints for better readability - Improve layout structure with proper flex properties and padding - Update documentation to reflect markdown syntax support in question field - Extract terminal dimensions for responsive layout calculations
1 parent f99b84a commit 8cf1572

3 files changed

Lines changed: 116 additions & 90 deletions

File tree

packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1873,7 +1873,7 @@ function TodoWrite(props: ToolProps<typeof TodoWriteTool>) {
18731873
}
18741874

18751875
function Question(props: ToolProps<typeof QuestionTool>) {
1876-
const { theme } = useTheme()
1876+
const { theme, syntax } = useTheme()
18771877
const count = createMemo(() => props.input.questions?.length ?? 0)
18781878

18791879
return (
@@ -1908,9 +1908,13 @@ function Question(props: ToolProps<typeof QuestionTool>) {
19081908
border={["right"]}
19091909
borderColor={theme.border}
19101910
>
1911-
<text fg={theme.textMuted} wrapMode="word">
1912-
{q.question}
1913-
</text>
1911+
<code
1912+
filetype="markdown"
1913+
drawUnstyledText={false}
1914+
syntaxStyle={syntax()}
1915+
content={q.question}
1916+
fg={theme.text}
1917+
/>
19141918
</box>
19151919
<box width={answerWidth()} paddingLeft={1} paddingRight={1} gap={isMulti() ? 1 : 0}>
19161920
<Show

packages/opencode/src/cli/cmd/tui/routes/session/question.tsx

Lines changed: 107 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createStore } from "solid-js/store"
22
import { createEffect, createMemo, For, on, Show } from "solid-js"
3-
import { useKeyboard } from "@opentui/solid"
3+
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
44
import type { TextareaRenderable } from "@opentui/core"
55
import { useKeybind } from "../../context/keybind"
66
import { useTheme } from "../../context/theme"
@@ -12,7 +12,8 @@ import { useDialog } from "../../ui/dialog"
1212

1313
export function QuestionPrompt(props: { request: QuestionRequest }) {
1414
const sdk = useSDK()
15-
const { theme } = useTheme()
15+
const { theme, syntax } = useTheme()
16+
const dimensions = useTerminalDimensions()
1617
const keybind = useKeybind()
1718
const bindings = useTextareaKeybindings()
1819

@@ -251,101 +252,119 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
251252
borderColor={theme.accent}
252253
customBorderChars={SplitBorder.customBorderChars}
253254
>
254-
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
255-
<Show when={props.request.from}>
256-
<box paddingLeft={1}>
257-
<text fg={theme.textMuted}>
258-
Question from <span style={{ fg: theme.accent }}>{props.request.from}</span>
259-
</text>
260-
</box>
261-
</Show>
262-
<Show when={!single()}>
263-
<box flexDirection="row" gap={1} paddingLeft={1}>
264-
<For each={questions()}>
265-
{(q, index) => {
266-
const isActive = () => index() === store.tab
267-
const isAnswered = () => {
268-
return (store.answers[index()]?.length ?? 0) > 0
269-
}
270-
return (
271-
<box
272-
paddingLeft={1}
273-
paddingRight={1}
274-
backgroundColor={isActive() ? theme.accent : theme.backgroundElement}
275-
>
276-
<text fg={isActive() ? theme.selectedListItemText : isAnswered() ? theme.text : theme.textMuted}>
277-
{q.header}
278-
</text>
279-
</box>
280-
)
281-
}}
282-
</For>
283-
<box paddingLeft={1} paddingRight={1} backgroundColor={confirm() ? theme.accent : theme.backgroundElement}>
284-
<text fg={confirm() ? theme.selectedListItemText : theme.textMuted}>Confirm</text>
255+
<Show when={props.request.from || !single()}>
256+
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1} flexShrink={0}>
257+
<Show when={props.request.from}>
258+
<box paddingLeft={1}>
259+
<text fg={theme.textMuted}>
260+
Question from <span style={{ fg: theme.accent }}>{props.request.from}</span>
261+
</text>
285262
</box>
286-
</box>
287-
</Show>
288-
289-
<Show when={!confirm()}>
290-
<box paddingLeft={1} gap={1}>
291-
<box>
292-
<text fg={theme.text}>{question()?.question}</text>
293-
</box>
294-
<box>
295-
<For each={options()}>
296-
{(opt, i) => {
297-
const active = () => i() === store.selected
298-
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
299-
const checkbox = () => (multi() ? (picked() ? "■" : "☐") : "")
263+
</Show>
264+
<Show when={!single()}>
265+
<box flexDirection="row" gap={1} paddingLeft={1}>
266+
<For each={questions()}>
267+
{(q, index) => {
268+
const isActive = () => index() === store.tab
269+
const isAnswered = () => {
270+
return (store.answers[index()]?.length ?? 0) > 0
271+
}
300272
return (
301-
<box>
302-
<box flexDirection="row" gap={1}>
303-
<box backgroundColor={active() ? theme.backgroundElement : undefined}>
304-
<text fg={active() ? theme.secondary : picked() ? theme.success : theme.text}>
305-
{multi() ? checkbox() : `${i() + 1}.`} {opt.label}
306-
</text>
307-
</box>
308-
</box>
309-
<box paddingLeft={3}>
310-
<text fg={theme.textMuted}>{opt.description}</text>
311-
</box>
273+
<box
274+
paddingLeft={1}
275+
paddingRight={1}
276+
backgroundColor={isActive() ? theme.accent : theme.backgroundElement}
277+
>
278+
<text fg={isActive() ? theme.selectedListItemText : isAnswered() ? theme.text : theme.textMuted}>
279+
{q.header}
280+
</text>
312281
</box>
313282
)
314283
}}
315284
</For>
316-
<box>
317-
<box flexDirection="row" gap={1}>
318-
<box backgroundColor={other() ? theme.backgroundElement : undefined}>
319-
<text fg={other() ? theme.secondary : customPicked() ? theme.success : theme.text}>
320-
{multi() ? (customPicked() ? "■" : "☐") : `${options().length + 1}.`} Type your own answer
321-
</text>
322-
</box>
323-
</box>
324-
<Show when={store.editing}>
325-
<box paddingLeft={3}>
326-
<textarea
327-
ref={(val: TextareaRenderable) => (textarea = val)}
328-
focused
329-
initialValue={input()}
330-
placeholder="Type your own answer"
331-
textColor={theme.text}
332-
focusedTextColor={theme.text}
333-
cursorColor={theme.primary}
334-
keyBindings={bindings()}
335-
/>
285+
<box
286+
paddingLeft={1}
287+
paddingRight={1}
288+
backgroundColor={confirm() ? theme.accent : theme.backgroundElement}
289+
>
290+
<text fg={confirm() ? theme.selectedListItemText : theme.textMuted}>Confirm</text>
291+
</box>
292+
</box>
293+
</Show>
294+
</box>
295+
</Show>
296+
297+
<Show when={!confirm()}>
298+
<box
299+
maxHeight={Math.floor(dimensions().height * 0.5)}
300+
overflow="scroll"
301+
paddingLeft={2}
302+
paddingRight={3}
303+
backgroundColor={theme.backgroundElement}
304+
>
305+
<code
306+
filetype="markdown"
307+
drawUnstyledText={false}
308+
syntaxStyle={syntax()}
309+
content={"\n" + (question()?.question ?? "") + "\n"}
310+
fg={theme.text}
311+
/>
312+
</box>
313+
<box paddingLeft={2} paddingRight={3} paddingTop={1} flexShrink={0}>
314+
<For each={options()}>
315+
{(opt, i) => {
316+
const active = () => i() === store.selected
317+
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
318+
const checkbox = () => (multi() ? (picked() ? "■" : "☐") : "")
319+
return (
320+
<box>
321+
<box flexDirection="row" gap={1}>
322+
<box backgroundColor={active() ? theme.backgroundElement : undefined}>
323+
<text fg={active() ? theme.secondary : picked() ? theme.success : theme.text}>
324+
{multi() ? checkbox() : `${i() + 1}.`} {opt.label}
325+
</text>
326+
</box>
336327
</box>
337-
</Show>
338-
<Show when={!store.editing && input()}>
339328
<box paddingLeft={3}>
340-
<text fg={theme.textMuted}>{input()}</text>
329+
<text fg={theme.textMuted}>{opt.description}</text>
341330
</box>
342-
</Show>
331+
</box>
332+
)
333+
}}
334+
</For>
335+
<box>
336+
<box flexDirection="row" gap={1}>
337+
<box backgroundColor={other() ? theme.backgroundElement : undefined}>
338+
<text fg={other() ? theme.secondary : customPicked() ? theme.success : theme.text}>
339+
{multi() ? (customPicked() ? "■" : "☐") : `${options().length + 1}.`} Type your own answer
340+
</text>
343341
</box>
344342
</box>
343+
<Show when={store.editing}>
344+
<box paddingLeft={3}>
345+
<textarea
346+
ref={(val: TextareaRenderable) => (textarea = val)}
347+
focused
348+
initialValue={input()}
349+
placeholder="Type your own answer"
350+
textColor={theme.text}
351+
focusedTextColor={theme.text}
352+
cursorColor={theme.primary}
353+
keyBindings={bindings()}
354+
/>
355+
</box>
356+
</Show>
357+
<Show when={!store.editing && input()}>
358+
<box paddingLeft={3}>
359+
<text fg={theme.textMuted}>{input()}</text>
360+
</box>
361+
</Show>
345362
</box>
346-
</Show>
363+
</box>
364+
</Show>
347365

348-
<Show when={confirm() && !single()}>
366+
<Show when={confirm() && !single()}>
367+
<box paddingLeft={2} paddingRight={3} flexGrow={1}>
349368
<box paddingLeft={1}>
350369
<text fg={theme.text}>Review</text>
351370
</box>
@@ -361,14 +380,16 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
361380
)
362381
}}
363382
</For>
364-
</Show>
365-
</box>
383+
</box>
384+
</Show>
385+
366386
<box
367387
flexDirection="row"
368388
flexShrink={0}
369389
gap={1}
370390
paddingLeft={2}
371391
paddingRight={3}
392+
paddingTop={1}
372393
paddingBottom={1}
373394
justifyContent="space-between"
374395
>

packages/opencode/src/tool/question.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ Usage notes:
99
- Answers are returned as arrays of labels; set `multiple: true` to allow selecting more than one
1010
- If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
1111
- Use the optional `from` parameter to identify who is asking the question (e.g. your agent name). This helps the user understand which agent is requesting input.
12+
- The `question` field supports full Markdown syntax including headings, bold, italic, code blocks with syntax highlighting, lists, links, and tables. Use this to format complex questions clearly.

0 commit comments

Comments
 (0)