Skip to content

Commit 52bfc9e

Browse files
Apply PR #15697: tweak(ui): make questions popup collapsible
2 parents 63837d7 + 611e616 commit 52bfc9e

File tree

2 files changed

+162
-86
lines changed

2 files changed

+162
-86
lines changed

packages/app/src/pages/session/composer/session-question-dock.tsx

Lines changed: 152 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useMutation } from "@tanstack/solid-query"
44
import { Button } from "@opencode-ai/ui/button"
55
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
66
import { Icon } from "@opencode-ai/ui/icon"
7+
import { IconButton } from "@opencode-ai/ui/icon-button"
78
import { showToast } from "@opencode-ai/ui/toast"
89
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
910
import { useLanguage } from "@/context/language"
@@ -73,6 +74,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
7374
customOn: cached?.customOn ?? ([] as boolean[]),
7475
editing: false,
7576
focus: 0,
77+
collapsed: false,
7678
})
7779

7880
let root: HTMLDivElement | undefined
@@ -87,6 +89,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
8789
const on = createMemo(() => store.customOn[store.tab] === true)
8890
const multi = createMemo(() => question()?.multiple === true)
8991
const count = createMemo(() => options().length + 1)
92+
const pickedCount = createMemo(() => store.answers[store.tab]?.length ?? 0)
9093

9194
const summary = createMemo(() => {
9295
const n = Math.min(store.tab + 1, total())
@@ -98,6 +101,8 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
98101

99102
const last = createMemo(() => store.tab >= total() - 1)
100103

104+
const fold = () => setStore("collapsed", (value) => !value)
105+
101106
const customUpdate = (value: string, selected: boolean = on()) => {
102107
const prev = input().trim()
103108
const next = value.trim()
@@ -426,9 +431,21 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
426431
ref={(el) => (root = el)}
427432
onKeyDown={nav}
428433
header={
429-
<>
434+
<div
435+
data-action="session-question-toggle"
436+
class="flex flex-1 min-w-0 items-center gap-2 cursor-default select-none"
437+
role="button"
438+
tabIndex={0}
439+
style={{ margin: "0 -10px", padding: "0 0 0 10px" }}
440+
onClick={fold}
441+
onKeyDown={(event) => {
442+
if (event.key !== "Enter" && event.key !== " ") return
443+
event.preventDefault()
444+
fold()
445+
}}
446+
>
430447
<div data-slot="question-header-title">{summary()}</div>
431-
<div data-slot="question-progress">
448+
<div data-slot="question-progress" class="ml-auto mr-1">
432449
<For each={questions()}>
433450
{(_, i) => (
434451
<button
@@ -437,13 +454,38 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
437454
data-active={i() === store.tab}
438455
data-answered={answered(i())}
439456
disabled={sending()}
440-
onClick={() => jump(i())}
457+
onMouseDown={(event) => {
458+
event.preventDefault()
459+
event.stopPropagation()
460+
}}
461+
onClick={(event) => {
462+
event.stopPropagation()
463+
jump(i())
464+
}}
441465
aria-label={`${language.t("ui.tool.questions")} ${i() + 1}`}
442466
/>
443467
)}
444468
</For>
445469
</div>
446-
</>
470+
<div>
471+
<IconButton
472+
data-action="session-question-toggle-button"
473+
icon="chevron-down"
474+
size="normal"
475+
variant="ghost"
476+
classList={{ "rotate-180": store.collapsed }}
477+
onMouseDown={(event) => {
478+
event.preventDefault()
479+
event.stopPropagation()
480+
}}
481+
onClick={(event) => {
482+
event.stopPropagation()
483+
fold()
484+
}}
485+
aria-label={store.collapsed ? language.t("session.todo.expand") : language.t("session.todo.collapse")}
486+
/>
487+
</div>
488+
</div>
447489
}
448490
footer={
449491
<>
@@ -469,99 +511,123 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
469511
</>
470512
}
471513
>
472-
<div data-slot="question-text">{question()?.question}</div>
473-
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
474-
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
514+
<div
515+
data-slot="question-text"
516+
class="cursor-default"
517+
classList={{
518+
"mb-6": store.collapsed && pickedCount() === 0,
519+
}}
520+
role={store.collapsed ? "button" : undefined}
521+
tabIndex={store.collapsed ? 0 : undefined}
522+
onClick={fold}
523+
onKeyDown={(event) => {
524+
if (!store.collapsed) return
525+
if (event.key !== "Enter" && event.key !== " ") return
526+
event.preventDefault()
527+
fold()
528+
}}
529+
>
530+
{question()?.question}
531+
</div>
532+
<Show when={store.collapsed && pickedCount() > 0}>
533+
<div data-slot="question-hint" class="cursor-default mb-6">
534+
{pickedCount()} answer{pickedCount() === 1 ? "" : "s"} selected
535+
</div>
475536
</Show>
476-
<div data-slot="question-options">
477-
<For each={options()}>
478-
{(opt, i) => (
479-
<Option
480-
multi={multi()}
481-
picked={picked(opt.label)}
482-
label={opt.label}
483-
description={opt.description}
484-
disabled={sending()}
485-
ref={(el) => (optsRef[i()] = el)}
486-
onFocus={() => setStore("focus", i())}
487-
onClick={() => selectOption(i())}
488-
/>
489-
)}
490-
</For>
491-
492-
<Show
493-
when={store.editing}
494-
fallback={
495-
<button
496-
type="button"
497-
ref={customRef}
537+
<div data-slot="question-answers" hidden={store.collapsed} aria-hidden={store.collapsed}>
538+
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
539+
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
540+
</Show>
541+
<div data-slot="question-options">
542+
<For each={options()}>
543+
{(opt, i) => (
544+
<Option
545+
multi={multi()}
546+
picked={picked(opt.label)}
547+
label={opt.label}
548+
description={opt.description}
549+
disabled={sending()}
550+
ref={(el) => (optsRef[i()] = el)}
551+
onFocus={() => setStore("focus", i())}
552+
onClick={() => selectOption(i())}
553+
/>
554+
)}
555+
</For>
556+
557+
<Show
558+
when={store.editing}
559+
fallback={
560+
<button
561+
type="button"
562+
ref={customRef}
563+
data-slot="question-option"
564+
data-custom="true"
565+
data-picked={on()}
566+
role={multi() ? "checkbox" : "radio"}
567+
aria-checked={on()}
568+
disabled={sending()}
569+
onFocus={() => setStore("focus", options().length)}
570+
onClick={customOpen}
571+
>
572+
<Mark multi={multi()} picked={on()} onClick={toggleCustomMark} />
573+
<span data-slot="question-option-main">
574+
<span data-slot="option-label">{customLabel()}</span>
575+
<span data-slot="option-description">{input() || customPlaceholder()}</span>
576+
</span>
577+
</button>
578+
}
579+
>
580+
<form
498581
data-slot="question-option"
499582
data-custom="true"
500583
data-picked={on()}
501584
role={multi() ? "checkbox" : "radio"}
502585
aria-checked={on()}
503-
disabled={sending()}
504-
onFocus={() => setStore("focus", options().length)}
505-
onClick={customOpen}
586+
onMouseDown={(e) => {
587+
if (sending()) {
588+
e.preventDefault()
589+
return
590+
}
591+
if (e.target instanceof HTMLTextAreaElement) return
592+
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
593+
if (input instanceof HTMLTextAreaElement) input.focus()
594+
}}
595+
onSubmit={(e) => {
596+
e.preventDefault()
597+
commitCustom()
598+
}}
506599
>
507600
<Mark multi={multi()} picked={on()} onClick={toggleCustomMark} />
508601
<span data-slot="question-option-main">
509602
<span data-slot="option-label">{customLabel()}</span>
510-
<span data-slot="option-description">{input() || customPlaceholder()}</span>
511-
</span>
512-
</button>
513-
}
514-
>
515-
<form
516-
data-slot="question-option"
517-
data-custom="true"
518-
data-picked={on()}
519-
role={multi() ? "checkbox" : "radio"}
520-
aria-checked={on()}
521-
onMouseDown={(e) => {
522-
if (sending()) {
523-
e.preventDefault()
524-
return
525-
}
526-
if (e.target instanceof HTMLTextAreaElement) return
527-
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
528-
if (input instanceof HTMLTextAreaElement) input.focus()
529-
}}
530-
onSubmit={(e) => {
531-
e.preventDefault()
532-
commitCustom()
533-
}}
534-
>
535-
<Mark multi={multi()} picked={on()} onClick={toggleCustomMark} />
536-
<span data-slot="question-option-main">
537-
<span data-slot="option-label">{customLabel()}</span>
538-
<textarea
539-
ref={focusCustom}
540-
data-slot="question-custom-input"
541-
placeholder={customPlaceholder()}
542-
value={input()}
543-
rows={1}
544-
disabled={sending()}
545-
onKeyDown={(e) => {
546-
if (e.key === "Escape") {
603+
<textarea
604+
ref={focusCustom}
605+
data-slot="question-custom-input"
606+
placeholder={customPlaceholder()}
607+
value={input()}
608+
rows={1}
609+
disabled={sending()}
610+
onKeyDown={(e) => {
611+
if (e.key === "Escape") {
612+
e.preventDefault()
613+
setStore("editing", false)
614+
focus(options().length)
615+
return
616+
}
617+
if ((e.metaKey || e.ctrlKey) && !e.altKey) return
618+
if (e.key !== "Enter" || e.shiftKey) return
547619
e.preventDefault()
548-
setStore("editing", false)
549-
focus(options().length)
550-
return
551-
}
552-
if ((e.metaKey || e.ctrlKey) && !e.altKey) return
553-
if (e.key !== "Enter" || e.shiftKey) return
554-
e.preventDefault()
555-
commitCustom()
556-
}}
557-
onInput={(e) => {
558-
customUpdate(e.currentTarget.value)
559-
resizeInput(e.currentTarget)
560-
}}
561-
/>
562-
</span>
563-
</form>
564-
</Show>
620+
commitCustom()
621+
}}
622+
onInput={(e) => {
623+
customUpdate(e.currentTarget.value)
624+
resizeInput(e.currentTarget)
625+
}}
626+
/>
627+
</span>
628+
</form>
629+
</Show>
630+
</div>
565631
</div>
566632
</DockPrompt>
567633
)

packages/ui/src/components/message-part.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,16 @@
10641064
white-space: normal;
10651065
}
10661066

1067+
[data-slot="question-option"][data-custom="true"] {
1068+
&[data-picked="true"] {
1069+
[data-slot="question-custom-input"]:focus-visible {
1070+
outline: none;
1071+
outline-offset: 0;
1072+
border-radius: 0;
1073+
}
1074+
}
1075+
}
1076+
10671077
[data-slot="question-custom"] {
10681078
display: flex;
10691079
flex-direction: column;

0 commit comments

Comments
 (0)